mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-25 18:55:19 +02:00
add reason to conversation as well
This commit is contained in:
parent
5a36653af1
commit
23d88aa7c0
12 changed files with 102 additions and 86 deletions
50
apps/rowboat/app/lib/components/reason-badge.tsx
Normal file
50
apps/rowboat/app/lib/components/reason-badge.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Turn } from "@/src/entities/models/turn";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export function ReasonBadge({
|
||||||
|
reason,
|
||||||
|
projectId
|
||||||
|
}: {
|
||||||
|
reason: z.infer<typeof Turn>['reason'];
|
||||||
|
projectId?: string;
|
||||||
|
}) {
|
||||||
|
const getReasonDisplay = () => {
|
||||||
|
switch (reason.type) {
|
||||||
|
case 'chat':
|
||||||
|
return { label: 'CHAT', color: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' };
|
||||||
|
case 'api':
|
||||||
|
return { label: 'API', color: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300' };
|
||||||
|
case 'job':
|
||||||
|
return {
|
||||||
|
label: `JOB: ${reason.jobId}`,
|
||||||
|
color: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300',
|
||||||
|
isJob: true,
|
||||||
|
jobId: reason.jobId
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return { label: 'UNKNOWN', color: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { label, color, isJob, jobId } = getReasonDisplay();
|
||||||
|
|
||||||
|
// Job reasons should ALWAYS be linked when we have a projectId
|
||||||
|
if (isJob && jobId && projectId) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={`/projects/${projectId}/jobs/${jobId}`}
|
||||||
|
className={`inline-flex items-center px-2 py-1 rounded-md text-xs font-mono font-medium ${color} hover:opacity-80 transition-opacity`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise render as a regular badge
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center px-2 py-1 rounded-md text-xs font-mono font-medium ${color}`}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -9,68 +9,7 @@ import { Turn } from "@/src/entities/models/turn";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { MessageDisplay } from "../../../../lib/components/message-display";
|
import { MessageDisplay } from "../../../../lib/components/message-display";
|
||||||
|
import { ReasonBadge } from "../../../../lib/components/reason-badge";
|
||||||
function TurnReason({ reason }: { reason: z.infer<typeof Turn>['reason'] }) {
|
|
||||||
const getReasonDisplay = () => {
|
|
||||||
switch (reason.type) {
|
|
||||||
case 'chat':
|
|
||||||
return { label: 'CHAT', color: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' };
|
|
||||||
case 'api':
|
|
||||||
return { label: 'API', color: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300' };
|
|
||||||
case 'job':
|
|
||||||
return { label: `JOB: ${reason.jobId}`, color: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300' };
|
|
||||||
default:
|
|
||||||
return { label: 'UNKNOWN', color: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300' };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { label, color } = getReasonDisplay();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className={`inline-flex items-center px-2 py-1 rounded-md text-xs font-mono font-medium ${color}`}>
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TurnReasonWithLink({ reason, projectId }: { reason: z.infer<typeof Turn>['reason']; projectId: string }) {
|
|
||||||
const getReasonDisplay = () => {
|
|
||||||
switch (reason.type) {
|
|
||||||
case 'chat':
|
|
||||||
return { label: 'CHAT', color: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' };
|
|
||||||
case 'api':
|
|
||||||
return { label: 'API', color: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300' };
|
|
||||||
case 'job':
|
|
||||||
return {
|
|
||||||
label: `JOB: ${reason.jobId}`,
|
|
||||||
color: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300',
|
|
||||||
isJob: true,
|
|
||||||
jobId: reason.jobId
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return { label: 'UNKNOWN', color: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300' };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { label, color, isJob, jobId } = getReasonDisplay();
|
|
||||||
|
|
||||||
if (isJob && jobId) {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
href={`/projects/${projectId}/jobs/${jobId}`}
|
|
||||||
className={`inline-flex items-center px-2 py-1 rounded-md text-xs font-mono font-medium ${color} hover:opacity-80 transition-opacity`}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className={`inline-flex items-center px-2 py-1 rounded-md text-xs font-mono font-medium ${color}`}>
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TurnContainer({ turn, index, projectId }: { turn: z.infer<typeof Turn>; index: number; projectId: string }) {
|
function TurnContainer({ turn, index, projectId }: { turn: z.infer<typeof Turn>; index: number; projectId: string }) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -82,7 +21,7 @@ function TurnContainer({ turn, index, projectId }: { turn: z.infer<typeof Turn>;
|
||||||
<span className="text-sm font-mono font-semibold text-gray-700 dark:text-gray-300">
|
<span className="text-sm font-mono font-semibold text-gray-700 dark:text-gray-300">
|
||||||
TURN #{index + 1}
|
TURN #{index + 1}
|
||||||
</span>
|
</span>
|
||||||
<TurnReasonWithLink reason={turn.reason} projectId={projectId} />
|
<ReasonBadge reason={turn.reason} projectId={projectId} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-500">
|
<div className="text-xs text-gray-500 dark:text-gray-500">
|
||||||
{new Date(turn.createdAt).toLocaleTimeString()}
|
{new Date(turn.createdAt).toLocaleTimeString()}
|
||||||
|
|
@ -199,6 +138,12 @@ export function ConversationView({ projectId, conversationId }: { projectId: str
|
||||||
{conversation.isLiveWorkflow ? 'Yes' : 'No'}
|
{conversation.isLiveWorkflow ? 'Yes' : 'No'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold text-gray-700 dark:text-gray-300">Reason:</span>
|
||||||
|
<span className="ml-2">
|
||||||
|
<ReasonBadge reason={conversation.reason} projectId={projectId} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { listConversations } from "@/app/actions/conversation_actions";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ListedConversationItem } from "@/src/application/repositories/conversations.repository.interface";
|
import { ListedConversationItem } from "@/src/application/repositories/conversations.repository.interface";
|
||||||
import { isToday, isThisWeek, isThisMonth } from "@/lib/utils/date";
|
import { isToday, isThisWeek, isThisMonth } from "@/lib/utils/date";
|
||||||
|
import { ReasonBadge } from "@/app/lib/components/reason-badge";
|
||||||
|
|
||||||
type ListedItem = z.infer<typeof ListedConversationItem>;
|
type ListedItem = z.infer<typeof ListedConversationItem>;
|
||||||
|
|
||||||
|
|
@ -101,12 +102,13 @@ export function ConversationsList({ projectId }: { projectId: string }) {
|
||||||
<thead className="bg-gray-50 dark:bg-gray-800/50">
|
<thead className="bg-gray-50 dark:bg-gray-800/50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Conversation</th>
|
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Conversation</th>
|
||||||
<th className="w-[30%] px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Created</th>
|
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Reason</th>
|
||||||
|
<th className="w-[25%] px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Created</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{group.map((c) => (
|
{group.map((c) => (
|
||||||
<tr key={c.id} className="hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors">
|
<tr key={c.id} className="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
||||||
<td className="px-6 py-4 text-left">
|
<td className="px-6 py-4 text-left">
|
||||||
<Link
|
<Link
|
||||||
href={`/projects/${projectId}/conversations/${c.id}`}
|
href={`/projects/${projectId}/conversations/${c.id}`}
|
||||||
|
|
@ -117,6 +119,9 @@ export function ConversationsList({ projectId }: { projectId: string }) {
|
||||||
{c.id}
|
{c.id}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-6 py-4 text-left">
|
||||||
|
<ReasonBadge reason={c.reason} projectId={projectId} />
|
||||||
|
</td>
|
||||||
<td className="px-6 py-4 text-left text-sm text-gray-600 dark:text-gray-300">
|
<td className="px-6 py-4 text-left text-sm text-gray-600 dark:text-gray-300">
|
||||||
{new Date(c.createdAt).toLocaleString()}
|
{new Date(c.createdAt).toLocaleString()}
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { PaginatedList } from "@/src/entities/common/paginated-list";
|
||||||
export const CreateConversationData = Conversation.pick({
|
export const CreateConversationData = Conversation.pick({
|
||||||
projectId: true,
|
projectId: true,
|
||||||
workflow: true,
|
workflow: true,
|
||||||
|
reason: true,
|
||||||
isLiveWorkflow: true,
|
isLiveWorkflow: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -17,6 +18,7 @@ export const AddTurnData = Turn.omit({
|
||||||
|
|
||||||
export const ListedConversationItem = Conversation.pick({
|
export const ListedConversationItem = Conversation.pick({
|
||||||
id: true,
|
id: true,
|
||||||
|
reason: true,
|
||||||
projectId: true,
|
projectId: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,14 @@ import { Conversation } from "@/src/entities/models/conversation";
|
||||||
import { Workflow } from "@/app/lib/types/workflow_types";
|
import { Workflow } from "@/app/lib/types/workflow_types";
|
||||||
import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface';
|
import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface';
|
||||||
import { IProjectActionAuthorizationPolicy } from '../../policies/project-action-authorization.policy';
|
import { IProjectActionAuthorizationPolicy } from '../../policies/project-action-authorization.policy';
|
||||||
|
import { Reason } from '@/src/entities/models/turn';
|
||||||
|
|
||||||
const inputSchema = z.object({
|
const inputSchema = z.object({
|
||||||
caller: z.enum(["user", "api", "job_worker"]),
|
caller: z.enum(["user", "api", "job_worker"]),
|
||||||
userId: z.string().optional(),
|
userId: z.string().optional(),
|
||||||
apiKey: z.string().optional(),
|
apiKey: z.string().optional(),
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
|
reason: Reason,
|
||||||
workflow: Workflow.optional(),
|
workflow: Workflow.optional(),
|
||||||
isLiveWorkflow: z.boolean().optional(),
|
isLiveWorkflow: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
@ -40,7 +42,7 @@ export class CreateConversationUseCase implements ICreateConversationUseCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(data: z.infer<typeof inputSchema>): Promise<z.infer<typeof Conversation>> {
|
async execute(data: z.infer<typeof inputSchema>): Promise<z.infer<typeof Conversation>> {
|
||||||
const { caller, userId, apiKey, projectId } = data;
|
const { caller, userId, apiKey, projectId, reason } = data;
|
||||||
let isLiveWorkflow = Boolean(data.isLiveWorkflow);
|
let isLiveWorkflow = Boolean(data.isLiveWorkflow);
|
||||||
let workflow = data.workflow;
|
let workflow = data.workflow;
|
||||||
|
|
||||||
|
|
@ -75,6 +77,7 @@ export class CreateConversationUseCase implements ICreateConversationUseCase {
|
||||||
// create conversation
|
// create conversation
|
||||||
return await this.conversationsRepository.create({
|
return await this.conversationsRepository.create({
|
||||||
projectId,
|
projectId,
|
||||||
|
reason,
|
||||||
workflow,
|
workflow,
|
||||||
isLiveWorkflow,
|
isLiveWorkflow,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Turn, TurnEvent } from "@/src/entities/models/turn";
|
import { Reason, Turn, TurnEvent } from "@/src/entities/models/turn";
|
||||||
import { USE_BILLING } from "@/app/lib/feature_flags";
|
import { USE_BILLING } from "@/app/lib/feature_flags";
|
||||||
import { authorize, getCustomerIdForProject, logUsage } from "@/app/lib/billing";
|
import { authorize, getCustomerIdForProject, logUsage } from "@/app/lib/billing";
|
||||||
import { NotFoundError } from '@/src/entities/errors/common';
|
import { NotFoundError } from '@/src/entities/errors/common';
|
||||||
|
|
@ -14,7 +14,7 @@ const inputSchema = z.object({
|
||||||
userId: z.string().optional(),
|
userId: z.string().optional(),
|
||||||
apiKey: z.string().optional(),
|
apiKey: z.string().optional(),
|
||||||
conversationId: z.string(),
|
conversationId: z.string(),
|
||||||
reason: Turn.shape.reason,
|
reason: Reason,
|
||||||
input: Turn.shape.input,
|
input: Turn.shape.input,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,10 @@ export class JobsWorker implements IJobsWorker {
|
||||||
const conversation = await this.createConversationUseCase.execute({
|
const conversation = await this.createConversationUseCase.execute({
|
||||||
caller: "job_worker",
|
caller: "job_worker",
|
||||||
projectId,
|
projectId,
|
||||||
|
reason: {
|
||||||
|
type: "job",
|
||||||
|
jobId: job.id,
|
||||||
|
},
|
||||||
workflow: job.input.workflow,
|
workflow: job.input.workflow,
|
||||||
isLiveWorkflow: true,
|
isLiveWorkflow: true,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Turn } from "./turn";
|
import { Reason, Turn } from "./turn";
|
||||||
import { Workflow } from "@/app/lib/types/workflow_types";
|
import { Workflow } from "@/app/lib/types/workflow_types";
|
||||||
|
|
||||||
export const Conversation = z.object({
|
export const Conversation = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
workflow: Workflow,
|
workflow: Workflow,
|
||||||
|
reason: Reason,
|
||||||
isLiveWorkflow: z.boolean(),
|
isLiveWorkflow: z.boolean(),
|
||||||
turns: z.array(Turn).optional(),
|
turns: z.array(Turn).optional(),
|
||||||
createdAt: z.string().datetime(),
|
createdAt: z.string().datetime(),
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ const jobReason = z.object({
|
||||||
jobId: z.string(),
|
jobId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const reason = z.discriminatedUnion("type", [
|
export const Reason = z.discriminatedUnion("type", [
|
||||||
chatReason,
|
chatReason,
|
||||||
apiReason,
|
apiReason,
|
||||||
jobReason,
|
jobReason,
|
||||||
|
|
@ -22,7 +22,7 @@ const reason = z.discriminatedUnion("type", [
|
||||||
|
|
||||||
export const Turn = z.object({
|
export const Turn = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
reason,
|
reason: Reason,
|
||||||
input: z.object({
|
input: z.object({
|
||||||
messages: z.array(Message),
|
messages: z.array(Message),
|
||||||
mockTools: z.record(z.string(), z.string()).nullable().optional(),
|
mockTools: z.record(z.string(), z.string()).nullable().optional(),
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ export class MongoDBConversationsRepository implements IConversationsRepository
|
||||||
projectId: 1,
|
projectId: 1,
|
||||||
createdAt: 1,
|
createdAt: 1,
|
||||||
updatedAt: 1,
|
updatedAt: 1,
|
||||||
|
reason: 1,
|
||||||
})
|
})
|
||||||
.toArray();
|
.toArray();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ export class CreatePlaygroundConversationController implements ICreatePlayground
|
||||||
return await this.createConversationUseCase.execute({
|
return await this.createConversationUseCase.execute({
|
||||||
caller: "user",
|
caller: "user",
|
||||||
userId,
|
userId,
|
||||||
|
reason: {
|
||||||
|
type: "chat",
|
||||||
|
},
|
||||||
projectId,
|
projectId,
|
||||||
workflow,
|
workflow,
|
||||||
isLiveWorkflow,
|
isLiveWorkflow,
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ export class RunTurnController implements IRunTurnController {
|
||||||
}
|
}
|
||||||
const { caller, userId, apiKey, projectId, input } = result.data;
|
const { caller, userId, apiKey, projectId, input } = result.data;
|
||||||
let conversationId = result.data.conversationId;
|
let conversationId = result.data.conversationId;
|
||||||
|
const reason = caller === "user" ? { type: "chat" as const } : { type: "api" as const };
|
||||||
|
|
||||||
// if conversationId is not provided, create conversation
|
// if conversationId is not provided, create conversation
|
||||||
if (!conversationId) {
|
if (!conversationId) {
|
||||||
|
|
@ -57,6 +58,7 @@ export class RunTurnController implements IRunTurnController {
|
||||||
userId,
|
userId,
|
||||||
apiKey,
|
apiKey,
|
||||||
projectId,
|
projectId,
|
||||||
|
reason,
|
||||||
});
|
});
|
||||||
conversationId = conversation.id;
|
conversationId = conversation.id;
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +69,7 @@ export class RunTurnController implements IRunTurnController {
|
||||||
userId,
|
userId,
|
||||||
apiKey,
|
apiKey,
|
||||||
conversationId,
|
conversationId,
|
||||||
reason: caller === "user" ? { type: "chat" } : { type: "api" },
|
reason,
|
||||||
input,
|
input,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue