add reason to conversation as well

This commit is contained in:
Ramnique Singh 2025-08-10 06:21:37 +05:30
parent 5a36653af1
commit 23d88aa7c0
12 changed files with 102 additions and 86 deletions

View 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>
);
}

View file

@ -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>

View file

@ -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>

View file

@ -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,

View file

@ -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,
}); });

View file

@ -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,
}); });

View file

@ -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,
}); });

View file

@ -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(),

View file

@ -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(),

View file

@ -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();

View file

@ -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,

View file

@ -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,
}); });