basic message handling

This commit is contained in:
abhi1992002
2026-01-29 18:11:42 +05:30
parent e0dfae5732
commit 73d8323fe4
5 changed files with 282 additions and 2 deletions

View File

@@ -113,7 +113,7 @@ class StreamToolOutputAvailable(StreamBaseResponse):
type: ResponseType = ResponseType.TOOL_OUTPUT_AVAILABLE
toolCallId: str = Field(..., description="Tool call ID this responds to")
output: str | dict[str, Any] = Field(..., description="Tool execution output")
# Additional fields for internal use (not part of AI SDK spec but useful)
# Keep these for internal backend use
toolName: str | None = Field(
default=None, description="Name of the tool that was executed"
)
@@ -121,6 +121,15 @@ class StreamToolOutputAvailable(StreamBaseResponse):
default=True, description="Whether the tool execution succeeded"
)
def to_sse(self) -> str:
"""Convert to SSE format, excluding non-spec fields."""
import json
data = {
"type": self.type.value,
"toolCallId": self.toolCallId,
"output": self.output,
}
return f"data: {json.dumps(data)}\n\n"
# ========== Other ==========

View File

@@ -30,6 +30,7 @@
"defaults"
],
"dependencies": {
"@ai-sdk/react": "3.0.61",
"@faker-js/faker": "10.0.0",
"@hookform/resolvers": "5.2.2",
"@next/third-parties": "15.4.6",
@@ -68,6 +69,7 @@
"@vercel/analytics": "1.5.0",
"@vercel/speed-insights": "1.2.0",
"@xyflow/react": "12.9.2",
"ai": "6.0.59",
"boring-avatars": "1.11.2",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",

View File

@@ -11,6 +11,9 @@ importers:
.:
dependencies:
'@ai-sdk/react':
specifier: 3.0.61
version: 3.0.61(react@18.3.1)(zod@3.25.76)
'@faker-js/faker':
specifier: 10.0.0
version: 10.0.0
@@ -125,6 +128,9 @@ importers:
'@xyflow/react':
specifier: 12.9.2
version: 12.9.2(@types/react@18.3.17)(immer@11.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
ai:
specifier: 6.0.59
version: 6.0.59(zod@3.25.76)
boring-avatars:
specifier: 1.11.2
version: 1.11.2
@@ -417,6 +423,28 @@ packages:
'@adobe/css-tools@4.4.4':
resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==}
'@ai-sdk/gateway@3.0.27':
resolution: {integrity: sha512-Pr+ApS9k6/jcR3kNltJNxo60OdYvnVU4DeRhzVtxUAYTXCHx4qO+qTMG9nNRn+El1acJnNRA//Su47srjXkT/w==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/provider-utils@4.0.10':
resolution: {integrity: sha512-VeDAiCH+ZK8Xs4hb9Cw7pHlujWNL52RKe8TExOkrw6Ir1AmfajBZTb9XUdKOZO08RwQElIKA8+Ltm+Gqfo8djQ==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/provider@3.0.5':
resolution: {integrity: sha512-2Xmoq6DBJqmSl80U6V9z5jJSJP7ehaJJQMy2iFUqTay06wdCqTnPVBBQbtEL8RCChenL+q5DC5H5WzU3vV3v8w==}
engines: {node: '>=18'}
'@ai-sdk/react@3.0.61':
resolution: {integrity: sha512-vCjZBnY2+TawFBXamSKt6elAt9n1MXMfcjSd9DSgT9peCJN27qNGVSXgaGNh/B3cUgeOktFfhB2GVmIqOjvmLQ==}
engines: {node: '>=18'}
peerDependencies:
react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
@@ -3691,6 +3719,10 @@ packages:
vue-router:
optional: true
'@vercel/oidc@3.1.0':
resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==}
engines: {node: '>= 20'}
'@vercel/speed-insights@1.2.0':
resolution: {integrity: sha512-y9GVzrUJ2xmgtQlzFP2KhVRoCglwfRQgjyfY607aU0hh0Un6d0OUyrJkjuAlsV18qR4zfoFPs/BiIj9YDS6Wzw==}
peerDependencies:
@@ -3872,6 +3904,12 @@ packages:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
ai@6.0.59:
resolution: {integrity: sha512-9SfCvcr4kVk4t8ZzIuyHpuL1hFYKsYMQfBSbBq3dipXPa+MphARvI8wHEjNaRqYl3JOsJbWxEBIMqHL0L92mUA==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
ajv-draft-04@1.0.0:
resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==}
peerDependencies:
@@ -4973,6 +5011,10 @@ packages:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
eventsource-parser@3.0.6:
resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
engines: {node: '>=18.0.0'}
evp_bytestokey@1.0.3:
resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==}
@@ -5697,6 +5739,9 @@ packages:
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
json-schema@0.4.0:
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
@@ -7438,6 +7483,11 @@ packages:
resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==}
hasBin: true
swr@2.3.8:
resolution: {integrity: sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w==}
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
@@ -7498,6 +7548,10 @@ packages:
third-party-capital@1.0.20:
resolution: {integrity: sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA==}
throttleit@2.1.0:
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
engines: {node: '>=18'}
timers-browserify@2.0.12:
resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==}
engines: {node: '>=0.6.0'}
@@ -8150,6 +8204,34 @@ snapshots:
'@adobe/css-tools@4.4.4': {}
'@ai-sdk/gateway@3.0.27(zod@3.25.76)':
dependencies:
'@ai-sdk/provider': 3.0.5
'@ai-sdk/provider-utils': 4.0.10(zod@3.25.76)
'@vercel/oidc': 3.1.0
zod: 3.25.76
'@ai-sdk/provider-utils@4.0.10(zod@3.25.76)':
dependencies:
'@ai-sdk/provider': 3.0.5
'@standard-schema/spec': 1.1.0
eventsource-parser: 3.0.6
zod: 3.25.76
'@ai-sdk/provider@3.0.5':
dependencies:
json-schema: 0.4.0
'@ai-sdk/react@3.0.61(react@18.3.1)(zod@3.25.76)':
dependencies:
'@ai-sdk/provider-utils': 4.0.10(zod@3.25.76)
ai: 6.0.59(zod@3.25.76)
react: 18.3.1
swr: 2.3.8(react@18.3.1)
throttleit: 2.1.0
transitivePeerDependencies:
- zod
'@alloc/quick-lru@5.2.0': {}
'@apidevtools/json-schema-ref-parser@14.0.1':
@@ -11796,6 +11878,8 @@ snapshots:
next: 15.4.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
'@vercel/oidc@3.1.0': {}
'@vercel/speed-insights@1.2.0(next@15.4.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
optionalDependencies:
next: 15.4.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -12023,6 +12107,14 @@ snapshots:
agent-base@7.1.4:
optional: true
ai@6.0.59(zod@3.25.76):
dependencies:
'@ai-sdk/gateway': 3.0.27(zod@3.25.76)
'@ai-sdk/provider': 3.0.5
'@ai-sdk/provider-utils': 4.0.10(zod@3.25.76)
'@opentelemetry/api': 1.9.0
zod: 3.25.76
ajv-draft-04@1.0.0(ajv@8.17.1):
optionalDependencies:
ajv: 8.17.1
@@ -13347,6 +13439,8 @@ snapshots:
events@3.3.0: {}
eventsource-parser@3.0.6: {}
evp_bytestokey@1.0.3:
dependencies:
md5.js: 1.3.5
@@ -14164,6 +14258,8 @@ snapshots:
json-schema-traverse@1.0.0: {}
json-schema@0.4.0: {}
json-stable-stringify-without-jsonify@1.0.1: {}
json5@1.0.2:
@@ -16341,6 +16437,12 @@ snapshots:
transitivePeerDependencies:
- encoding
swr@2.3.8(react@18.3.1):
dependencies:
dequal: 2.0.3
react: 18.3.1
use-sync-external-store: 1.6.0(react@18.3.1)
symbol-tree@3.2.4:
optional: true
@@ -16413,6 +16515,8 @@ snapshots:
third-party-capital@1.0.20: {}
throttleit@2.1.0: {}
timers-browserify@2.0.12:
dependencies:
setimmediate: 1.0.5

View File

@@ -0,0 +1,165 @@
'use client';
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { useState, useMemo } from 'react';
import { parseAsString, useQueryState } from 'nuqs';
import { postV2CreateSession } from '@/app/api/__generated__/endpoints/chat/chat';
export default function Page() {
const [sessionId, setSessionId] = useQueryState('sessionId', parseAsString);
const [isCreating, setIsCreating] = useState(false);
const [input, setInput] = useState('');
const transport = useMemo(() => {
if (!sessionId) return null;
return new DefaultChatTransport({
api: `/api/chat/sessions/${sessionId}/stream`,
prepareSendMessagesRequest: ({ id, messages, trigger, messageId }) => {
const last = messages[messages.length - 1];
return {
body: {
message: last.parts?.map((p) => (p.type === 'text' ? p.text : '')).join(''),
is_user_message: last.role === 'user',
context: null,
},
};
},
});
}, [sessionId]);
const { messages, sendMessage, status, error } = useChat({
transport: transport ?? undefined,
});
async function createSession(): Promise<string | null> {
setIsCreating(true);
try {
const response = await postV2CreateSession({
body: JSON.stringify({}),
});
if (response.status === 200 && response.data?.id) {
return response.data.id;
}
console.error('[Copilot2] Failed to create session:', response);
return null;
} catch (error) {
console.error('[Copilot2] Error creating session:', error);
return null;
} finally {
setIsCreating(false);
}
}
async function handleNewSession() {
const newSessionId = await createSession();
if (newSessionId) {
setSessionId(newSessionId);
}
}
async function handleStartChat(prompt: string) {
if (!prompt.trim() || isCreating) return;
const newSessionId = await createSession();
if (newSessionId) {
await setSessionId(newSessionId);
sendMessage({ text: prompt });
setInput('');
}
}
// Show landing page when no session exists
if (!sessionId) {
return (
<div className="flex h-full">
<div className="flex w-64 flex-col border-r border-zinc-200 bg-zinc-50 p-4">
<button
onClick={handleNewSession}
disabled={isCreating}
className="rounded-md bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
>
{isCreating ? 'Creating...' : 'New Session'}
</button>
</div>
<div className="flex h-full flex-1 flex-col items-center justify-center bg-zinc-100 p-4">
<h2 className="mb-4 text-xl font-semibold text-zinc-700">Start a new conversation</h2>
<form
onSubmit={(e) => {
e.preventDefault();
handleStartChat(input);
}}
className="w-full max-w-md"
>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={isCreating}
placeholder="Type your message to start..."
className="w-full rounded-md border border-zinc-300 px-4 py-2"
/>
<button
type="submit"
disabled={isCreating || !input.trim()}
className="mt-2 w-full rounded-md bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
>
{isCreating ? 'Starting...' : 'Start Chat'}
</button>
</form>
</div>
</div>
);
}
return (
<div className="flex h-full">
{/* Sidebar */}
<div className="flex w-64 flex-col border-r border-zinc-200 bg-zinc-50 p-4">
<button
onClick={handleNewSession}
disabled={isCreating}
className="rounded-md bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
>
{isCreating ? 'Creating...' : 'New Session'}
</button>
</div>
{/* Chat area */}
<div className="flex h-full flex-1 flex-col bg-zinc-100 p-4">
<div className="text-sm text-zinc-500 mb-2">Session ID: {sessionId}</div>
<div className="flex-1 overflow-y-auto">
{messages.map((message) => (
<div key={message.id}>
{message.role === 'user' ? 'User: ' : 'AI: '}
{message.parts.map((part, index) =>
part.type === 'text' ? <p key={index}>{part.text}</p> : null,
)}
</div>
))}
{error && <div className="text-red-500">Error: {error.message}</div>}
</div>
<form
onSubmit={(e) => {
e.preventDefault();
if (input.trim()) {
sendMessage({ text: input });
setInput('');
}
}}
>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={status !== 'ready'}
placeholder="Say something..."
/>
<button type="submit" disabled={status !== 'ready'}>
Submit
</button>
</form>
</div>
</div>
);
}

View File

@@ -47,7 +47,7 @@ const mockFlags = {
[Flag.AGENT_FAVORITING]: false,
[Flag.MARKETPLACE_SEARCH_TERMS]: DEFAULT_SEARCH_TERMS,
[Flag.ENABLE_PLATFORM_PAYMENT]: false,
[Flag.CHAT]: false,
[Flag.CHAT]: true,
};
export function useGetFlag<T extends Flag>(flag: T): FlagValues[T] | null {