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 Link from "next/link";
|
||||
import { MessageDisplay } from "../../../../lib/components/message-display";
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
import { ReasonBadge } from "../../../../lib/components/reason-badge";
|
||||
|
||||
function TurnContainer({ turn, index, projectId }: { turn: z.infer<typeof Turn>; index: number; projectId: string }) {
|
||||
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">
|
||||
TURN #{index + 1}
|
||||
</span>
|
||||
<TurnReasonWithLink reason={turn.reason} projectId={projectId} />
|
||||
<ReasonBadge reason={turn.reason} projectId={projectId} />
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-500">
|
||||
{new Date(turn.createdAt).toLocaleTimeString()}
|
||||
|
|
@ -199,6 +138,12 @@ export function ConversationView({ projectId, conversationId }: { projectId: str
|
|||
{conversation.isLiveWorkflow ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</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>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { listConversations } from "@/app/actions/conversation_actions";
|
|||
import { z } from "zod";
|
||||
import { ListedConversationItem } from "@/src/application/repositories/conversations.repository.interface";
|
||||
import { isToday, isThisWeek, isThisMonth } from "@/lib/utils/date";
|
||||
import { ReasonBadge } from "@/app/lib/components/reason-badge";
|
||||
|
||||
type ListedItem = z.infer<typeof ListedConversationItem>;
|
||||
|
||||
|
|
@ -101,26 +102,30 @@ export function ConversationsList({ projectId }: { projectId: string }) {
|
|||
<thead className="bg-gray-50 dark:bg-gray-800/50">
|
||||
<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="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>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{group.map((c) => (
|
||||
<tr key={c.id} className="hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors">
|
||||
<td className="px-6 py-4 text-left">
|
||||
<Link
|
||||
href={`/projects/${projectId}/conversations/${c.id}`}
|
||||
size="lg"
|
||||
isBlock
|
||||
className="text-sm text-gray-900 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400 truncate block"
|
||||
>
|
||||
{c.id}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-left text-sm text-gray-600 dark:text-gray-300">
|
||||
{new Date(c.createdAt).toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
<tr key={c.id} className="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
||||
<td className="px-6 py-4 text-left">
|
||||
<Link
|
||||
href={`/projects/${projectId}/conversations/${c.id}`}
|
||||
size="lg"
|
||||
isBlock
|
||||
className="text-sm text-gray-900 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400 truncate block"
|
||||
>
|
||||
{c.id}
|
||||
</Link>
|
||||
</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">
|
||||
{new Date(c.createdAt).toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { PaginatedList } from "@/src/entities/common/paginated-list";
|
|||
export const CreateConversationData = Conversation.pick({
|
||||
projectId: true,
|
||||
workflow: true,
|
||||
reason: true,
|
||||
isLiveWorkflow: true,
|
||||
});
|
||||
|
||||
|
|
@ -17,6 +18,7 @@ export const AddTurnData = Turn.omit({
|
|||
|
||||
export const ListedConversationItem = Conversation.pick({
|
||||
id: true,
|
||||
reason: true,
|
||||
projectId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ import { Conversation } from "@/src/entities/models/conversation";
|
|||
import { Workflow } from "@/app/lib/types/workflow_types";
|
||||
import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface';
|
||||
import { IProjectActionAuthorizationPolicy } from '../../policies/project-action-authorization.policy';
|
||||
import { Reason } from '@/src/entities/models/turn';
|
||||
|
||||
const inputSchema = z.object({
|
||||
caller: z.enum(["user", "api", "job_worker"]),
|
||||
userId: z.string().optional(),
|
||||
apiKey: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
reason: Reason,
|
||||
workflow: Workflow.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>> {
|
||||
const { caller, userId, apiKey, projectId } = data;
|
||||
const { caller, userId, apiKey, projectId, reason } = data;
|
||||
let isLiveWorkflow = Boolean(data.isLiveWorkflow);
|
||||
let workflow = data.workflow;
|
||||
|
||||
|
|
@ -75,6 +77,7 @@ export class CreateConversationUseCase implements ICreateConversationUseCase {
|
|||
// create conversation
|
||||
return await this.conversationsRepository.create({
|
||||
projectId,
|
||||
reason,
|
||||
workflow,
|
||||
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 { authorize, getCustomerIdForProject, logUsage } from "@/app/lib/billing";
|
||||
import { NotFoundError } from '@/src/entities/errors/common';
|
||||
|
|
@ -14,7 +14,7 @@ const inputSchema = z.object({
|
|||
userId: z.string().optional(),
|
||||
apiKey: z.string().optional(),
|
||||
conversationId: z.string(),
|
||||
reason: Turn.shape.reason,
|
||||
reason: Reason,
|
||||
input: Turn.shape.input,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,10 @@ export class JobsWorker implements IJobsWorker {
|
|||
const conversation = await this.createConversationUseCase.execute({
|
||||
caller: "job_worker",
|
||||
projectId,
|
||||
reason: {
|
||||
type: "job",
|
||||
jobId: job.id,
|
||||
},
|
||||
workflow: job.input.workflow,
|
||||
isLiveWorkflow: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { z } from "zod";
|
||||
import { Turn } from "./turn";
|
||||
import { Reason, Turn } from "./turn";
|
||||
import { Workflow } from "@/app/lib/types/workflow_types";
|
||||
|
||||
export const Conversation = z.object({
|
||||
id: z.string(),
|
||||
projectId: z.string(),
|
||||
workflow: Workflow,
|
||||
reason: Reason,
|
||||
isLiveWorkflow: z.boolean(),
|
||||
turns: z.array(Turn).optional(),
|
||||
createdAt: z.string().datetime(),
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const jobReason = z.object({
|
|||
jobId: z.string(),
|
||||
});
|
||||
|
||||
const reason = z.discriminatedUnion("type", [
|
||||
export const Reason = z.discriminatedUnion("type", [
|
||||
chatReason,
|
||||
apiReason,
|
||||
jobReason,
|
||||
|
|
@ -22,7 +22,7 @@ const reason = z.discriminatedUnion("type", [
|
|||
|
||||
export const Turn = z.object({
|
||||
id: z.string(),
|
||||
reason,
|
||||
reason: Reason,
|
||||
input: z.object({
|
||||
messages: z.array(Message),
|
||||
mockTools: z.record(z.string(), z.string()).nullable().optional(),
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ export class MongoDBConversationsRepository implements IConversationsRepository
|
|||
projectId: 1,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
reason: 1,
|
||||
})
|
||||
.toArray();
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ export class CreatePlaygroundConversationController implements ICreatePlayground
|
|||
return await this.createConversationUseCase.execute({
|
||||
caller: "user",
|
||||
userId,
|
||||
reason: {
|
||||
type: "chat",
|
||||
},
|
||||
projectId,
|
||||
workflow,
|
||||
isLiveWorkflow,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export class RunTurnController implements IRunTurnController {
|
|||
}
|
||||
const { caller, userId, apiKey, projectId, input } = result.data;
|
||||
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) {
|
||||
|
|
@ -57,6 +58,7 @@ export class RunTurnController implements IRunTurnController {
|
|||
userId,
|
||||
apiKey,
|
||||
projectId,
|
||||
reason,
|
||||
});
|
||||
conversationId = conversation.id;
|
||||
}
|
||||
|
|
@ -67,7 +69,7 @@ export class RunTurnController implements IRunTurnController {
|
|||
userId,
|
||||
apiKey,
|
||||
conversationId,
|
||||
reason: caller === "user" ? { type: "chat" } : { type: "api" },
|
||||
reason,
|
||||
input,
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue