@fragno-dev/chatno 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +23 -0
- package/.turbo/turbo-types$colon$check.log +1 -0
- package/CHANGELOG.md +29 -0
- package/dist/browser/index.d.ts +73 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +119 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/node/index.d.ts +73 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +74 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/server/chatno-api.js +108 -0
- package/dist/node/server/chatno-api.js.map +1 -0
- package/package.json +36 -0
- package/src/index.ts +110 -0
- package/src/message-service.ts +18 -0
- package/src/server/chatno-api.ts +160 -0
- package/tsconfig.json +10 -0
- package/tsdown.config.ts +26 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
$ tsdown
|
|
2
|
+
[34mℹ[39m tsdown [2mv0.15.5[22m powered by rolldown [2mv1.0.0-beta.40[22m
|
|
3
|
+
rollup
|
|
4
|
+
[34mℹ[39m Using tsdown config: [4m/home/runner/work/fragno/fragno/packages/chatno/tsdown.config.ts[24m
|
|
5
|
+
[34mℹ[39m entry: [34msrc/index.ts[39m
|
|
6
|
+
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
7
|
+
[34mℹ[39m entry: [34msrc/index.ts[39m
|
|
8
|
+
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
9
|
+
[34mℹ[39m Build start
|
|
10
|
+
[34mℹ[39m [2mdist/browser/[22m[1mindex.js[22m [2m 3.62 kB[22m [2m│ gzip: 1.23 kB[22m
|
|
11
|
+
[34mℹ[39m [2mdist/browser/[22mindex.js.map [2m12.00 kB[22m [2m│ gzip: 3.58 kB[22m
|
|
12
|
+
[34mℹ[39m [2mdist/browser/[22mindex.d.ts.map [2m 0.40 kB[22m [2m│ gzip: 0.23 kB[22m
|
|
13
|
+
[34mℹ[39m [2mdist/browser/[22m[32m[1mindex.d.ts[22m[39m [2m 2.83 kB[22m [2m│ gzip: 0.89 kB[22m
|
|
14
|
+
[34mℹ[39m 4 files, total: 18.84 kB
|
|
15
|
+
[32m✔[39m Build complete in [32m15780ms[39m
|
|
16
|
+
[34mℹ[39m [2mdist/node/[22m[1mindex.js[22m [2m2.42 kB[22m [2m│ gzip: 1.06 kB[22m
|
|
17
|
+
[34mℹ[39m [2mdist/node/[22mserver/chatno-api.js.map [2m6.89 kB[22m [2m│ gzip: 2.02 kB[22m
|
|
18
|
+
[34mℹ[39m [2mdist/node/[22mindex.js.map [2m4.61 kB[22m [2m│ gzip: 1.80 kB[22m
|
|
19
|
+
[34mℹ[39m [2mdist/node/[22mserver/chatno-api.js [2m3.21 kB[22m [2m│ gzip: 1.06 kB[22m
|
|
20
|
+
[34mℹ[39m [2mdist/node/[22mindex.d.ts.map [2m0.40 kB[22m [2m│ gzip: 0.23 kB[22m
|
|
21
|
+
[34mℹ[39m [2mdist/node/[22m[32m[1mindex.d.ts[22m[39m [2m2.83 kB[22m [2m│ gzip: 0.89 kB[22m
|
|
22
|
+
[34mℹ[39m 6 files, total: 20.35 kB
|
|
23
|
+
[32m✔[39m Build complete in [32m15866ms[39m
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
$ tsc --noEmit
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @fragno-dev/chatno
|
|
2
|
+
|
|
3
|
+
## 0.0.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [b9450b1]
|
|
8
|
+
- @fragno-dev/core@0.0.5
|
|
9
|
+
|
|
10
|
+
## 0.0.4
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [1dae6f9]
|
|
15
|
+
- @fragno-dev/core@0.0.4
|
|
16
|
+
|
|
17
|
+
## 0.0.3
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Updated dependencies [604caff]
|
|
22
|
+
- @fragno-dev/core@0.0.3
|
|
23
|
+
|
|
24
|
+
## 0.0.2
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- Updated dependencies [2052919]
|
|
29
|
+
- @fragno-dev/core@0.0.2
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as _fragno_dev_core0 from "@fragno-dev/core";
|
|
2
|
+
import { FragnoPublicClientConfig, FragnoPublicConfig } from "@fragno-dev/core";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import * as _fragno_dev_core_client0 from "@fragno-dev/core/client";
|
|
5
|
+
import OpenAI from "openai";
|
|
6
|
+
import * as nanostores0 from "nanostores";
|
|
7
|
+
import { FragnoRouteConfig } from "@fragno-dev/core/api";
|
|
8
|
+
|
|
9
|
+
//#region src/index.d.ts
|
|
10
|
+
interface ChatnoServerConfig {
|
|
11
|
+
openaiApiKey: string;
|
|
12
|
+
model?: "gpt-5-mini" | "4o-mini" | "gpt-5-nano";
|
|
13
|
+
systemPrompt?: string;
|
|
14
|
+
}
|
|
15
|
+
declare function createChatno(chatnoConfig: ChatnoServerConfig, fragnoConfig?: FragnoPublicConfig): _fragno_dev_core0.FragnoInstantiatedFragment<[_fragno_dev_core0.FragnoRouteConfig<"POST", "/chat/stream", z.ZodObject<{
|
|
16
|
+
messages: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
17
|
+
type: z.ZodLiteral<"chat">;
|
|
18
|
+
id: z.ZodString;
|
|
19
|
+
role: z.ZodEnum<{
|
|
20
|
+
user: "user";
|
|
21
|
+
assistant: "assistant";
|
|
22
|
+
}>;
|
|
23
|
+
content: z.ZodString;
|
|
24
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
25
|
+
type: z.ZodLiteral<"functionCall">;
|
|
26
|
+
id: z.ZodString;
|
|
27
|
+
functionCallId: z.ZodString;
|
|
28
|
+
name: z.ZodString;
|
|
29
|
+
arguments: z.ZodString;
|
|
30
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
31
|
+
type: z.ZodLiteral<"functionCallOutput">;
|
|
32
|
+
id: z.ZodString;
|
|
33
|
+
functionCallId: z.ZodString;
|
|
34
|
+
output: z.ZodString;
|
|
35
|
+
status: z.ZodEnum<{
|
|
36
|
+
inProgress: "inProgress";
|
|
37
|
+
completed: "completed";
|
|
38
|
+
incomplete: "incomplete";
|
|
39
|
+
}>;
|
|
40
|
+
}, z.core.$strip>], "type">>;
|
|
41
|
+
}, z.core.$strip>, z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
42
|
+
type: z.ZodLiteral<"response.output_text.delta">;
|
|
43
|
+
content_index: z.ZodNumber;
|
|
44
|
+
delta: z.ZodString;
|
|
45
|
+
item_id: z.ZodString;
|
|
46
|
+
output_index: z.ZodNumber;
|
|
47
|
+
sequence_number: z.ZodNumber;
|
|
48
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
49
|
+
type: z.ZodLiteral<"response.output_text.done">;
|
|
50
|
+
content_index: z.ZodNumber;
|
|
51
|
+
item_id: z.ZodString;
|
|
52
|
+
output_index: z.ZodNumber;
|
|
53
|
+
sequence_number: z.ZodNumber;
|
|
54
|
+
text: z.ZodString;
|
|
55
|
+
}, z.core.$strip>], "type">>, string, string>, _fragno_dev_core0.FragnoRouteConfig<"GET", "/health", undefined, z.ZodObject<{
|
|
56
|
+
status: z.ZodLiteral<"ok">;
|
|
57
|
+
}, z.core.$strip>, string, string>, _fragno_dev_core0.FragnoRouteConfig<"GET", "/simple-stream", undefined, z.ZodArray<z.ZodObject<{
|
|
58
|
+
message: z.ZodString;
|
|
59
|
+
}, z.core.$strip>>, string, string>], {
|
|
60
|
+
openaiClient: OpenAI;
|
|
61
|
+
}, {
|
|
62
|
+
getOpenAIURL: () => string;
|
|
63
|
+
}>;
|
|
64
|
+
declare function createChatnoClient(fragnoConfig?: FragnoPublicClientConfig): {
|
|
65
|
+
useSendMessage: _fragno_dev_core_client0.FragnoStoreData<{
|
|
66
|
+
readonly response: nanostores0.ReadableAtom<string>;
|
|
67
|
+
readonly responseLoading: nanostores0.ReadableAtom<boolean | undefined>;
|
|
68
|
+
readonly sendMessage: (message: string) => void;
|
|
69
|
+
}>;
|
|
70
|
+
};
|
|
71
|
+
//#endregion
|
|
72
|
+
export { ChatnoServerConfig, type FragnoRouteConfig, createChatno, createChatnoClient };
|
|
73
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;UAaiB,kBAAA;;;;;iBAsDD,YAAA,eACA,mCACA,uCAAuB,8CAAA,0CAAA,CAAA,CAAA;EAxDtB,QAAA,YAAA,wBAAkB,CAAA,YAAA,CAAA;IAsDnB,IAAA,cAAY,CAAA,MAAA,CAAA;IAAA,EAAA,aAAA;IACZ,IAAA,WAAA,CAAA;MACA,IAAA,EAAA,MAAA;;;;;;;;;;;;;;;;;;;;;;;;OAAuB,aAAA;;;;;;;;;;;;;;;;;;;;iBAWvB,kBAAA,gBAAiC;;uBAA6B,WAAA,CAAA"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createFragment, defineFragment, defineRoute, defineRoutes } from "@fragno-dev/core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createClientBuilder } from "@fragno-dev/core/client";
|
|
4
|
+
import OpenAI from "openai";
|
|
5
|
+
import { computed } from "nanostores";
|
|
6
|
+
|
|
7
|
+
//#region src/server/chatno-api.ts
|
|
8
|
+
const ChatMessageSchema = z.object({
|
|
9
|
+
type: z.literal("chat"),
|
|
10
|
+
id: z.string(),
|
|
11
|
+
role: z.enum(["user", "assistant"]),
|
|
12
|
+
content: z.string()
|
|
13
|
+
});
|
|
14
|
+
const FunctionCallMessageSchema = z.object({
|
|
15
|
+
type: z.literal("functionCall"),
|
|
16
|
+
id: z.string(),
|
|
17
|
+
functionCallId: z.string(),
|
|
18
|
+
name: z.string(),
|
|
19
|
+
arguments: z.string()
|
|
20
|
+
});
|
|
21
|
+
const FunctionCallOutputMessageSchema = z.object({
|
|
22
|
+
type: z.literal("functionCallOutput"),
|
|
23
|
+
id: z.string(),
|
|
24
|
+
functionCallId: z.string(),
|
|
25
|
+
output: z.string(),
|
|
26
|
+
status: z.enum([
|
|
27
|
+
"inProgress",
|
|
28
|
+
"completed",
|
|
29
|
+
"incomplete"
|
|
30
|
+
])
|
|
31
|
+
});
|
|
32
|
+
const InputMessageSchema = z.discriminatedUnion("type", [
|
|
33
|
+
ChatMessageSchema,
|
|
34
|
+
FunctionCallMessageSchema,
|
|
35
|
+
FunctionCallOutputMessageSchema
|
|
36
|
+
]);
|
|
37
|
+
const ChatStreamRequestSchema = z.object({ messages: z.array(InputMessageSchema) });
|
|
38
|
+
const ResponseTextDeltaEventSchema = z.object({
|
|
39
|
+
type: z.literal("response.output_text.delta"),
|
|
40
|
+
content_index: z.number(),
|
|
41
|
+
delta: z.string(),
|
|
42
|
+
item_id: z.string(),
|
|
43
|
+
output_index: z.number(),
|
|
44
|
+
sequence_number: z.number()
|
|
45
|
+
});
|
|
46
|
+
const ResponseTextDoneEventSchema = z.object({
|
|
47
|
+
type: z.literal("response.output_text.done"),
|
|
48
|
+
content_index: z.number(),
|
|
49
|
+
item_id: z.string(),
|
|
50
|
+
output_index: z.number(),
|
|
51
|
+
sequence_number: z.number(),
|
|
52
|
+
text: z.string()
|
|
53
|
+
});
|
|
54
|
+
const ResponseEventSchema = z.discriminatedUnion("type", [ResponseTextDeltaEventSchema, ResponseTextDoneEventSchema]);
|
|
55
|
+
const chatRouteFactory = defineRoutes().create(({ config, deps }) => {
|
|
56
|
+
return [defineRoute({
|
|
57
|
+
method: "POST",
|
|
58
|
+
path: "/chat/stream",
|
|
59
|
+
inputSchema: ChatStreamRequestSchema,
|
|
60
|
+
outputSchema: z.array(ResponseEventSchema),
|
|
61
|
+
handler: () => {}
|
|
62
|
+
})];
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/index.ts
|
|
67
|
+
const healthRoute = defineRoute({
|
|
68
|
+
method: "GET",
|
|
69
|
+
path: "/health",
|
|
70
|
+
outputSchema: z.object({ status: z.literal("ok") }),
|
|
71
|
+
handler: () => {}
|
|
72
|
+
});
|
|
73
|
+
const simpleStreamRoute = defineRoute({
|
|
74
|
+
method: "GET",
|
|
75
|
+
path: "/simple-stream",
|
|
76
|
+
outputSchema: z.array(z.object({ message: z.string() })),
|
|
77
|
+
handler: () => {}
|
|
78
|
+
});
|
|
79
|
+
const DEFAULT_SYSTEM_PROMPT = `You are an AI assistant integrated into a dashboard.`;
|
|
80
|
+
const chatnoDefinition = defineFragment("chatno").withDependencies(() => {}).withServices(() => {});
|
|
81
|
+
const routes = [
|
|
82
|
+
chatRouteFactory,
|
|
83
|
+
healthRoute,
|
|
84
|
+
simpleStreamRoute
|
|
85
|
+
];
|
|
86
|
+
function createChatno(chatnoConfig, fragnoConfig = {}) {
|
|
87
|
+
const config = {
|
|
88
|
+
model: chatnoConfig.model ?? "gpt-5-nano",
|
|
89
|
+
systemPrompt: chatnoConfig.systemPrompt ?? DEFAULT_SYSTEM_PROMPT
|
|
90
|
+
};
|
|
91
|
+
return createFragment(chatnoDefinition, {
|
|
92
|
+
...chatnoConfig,
|
|
93
|
+
...config
|
|
94
|
+
}, routes, fragnoConfig);
|
|
95
|
+
}
|
|
96
|
+
function createChatnoClient(fragnoConfig = {}) {
|
|
97
|
+
const cb = createClientBuilder(chatnoDefinition, fragnoConfig, routes);
|
|
98
|
+
const chatStream = cb.createMutator("POST", "/chat/stream");
|
|
99
|
+
const aggregatedMessage = computed(chatStream.mutatorStore, ({ data }) => {
|
|
100
|
+
return (data ?? []).filter((item) => item.type === "response.output_text.delta").map((item) => item.delta).join("");
|
|
101
|
+
});
|
|
102
|
+
function sendMessage(message) {
|
|
103
|
+
chatStream.mutatorStore.mutate({ body: { messages: [{
|
|
104
|
+
type: "chat",
|
|
105
|
+
id: crypto.randomUUID(),
|
|
106
|
+
role: "user",
|
|
107
|
+
content: message
|
|
108
|
+
}] } });
|
|
109
|
+
}
|
|
110
|
+
return { useSendMessage: cb.createStore({
|
|
111
|
+
response: aggregatedMessage,
|
|
112
|
+
responseLoading: computed(chatStream.mutatorStore, ({ loading }) => loading),
|
|
113
|
+
sendMessage
|
|
114
|
+
}) };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
//#endregion
|
|
118
|
+
export { createChatno, createChatnoClient };
|
|
119
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["OpenAI","z","defineRoute","defineRoutes","ChatMessageSchema","object","type","literal","id","string","role","enum","content","FunctionCallMessageSchema","functionCallId","name","arguments","FunctionCallOutputMessageSchema","output","status","InputMessageSchema","discriminatedUnion","ChatStreamRequestSchema","messages","array","ResponseTextDeltaEventSchema","content_index","number","delta","item_id","output_index","sequence_number","ResponseTextDoneEventSchema","text","ResponseEventSchema","ChatRouteConfig","model","systemPrompt","ChatRouteDeps","openaiClient","chatRouteFactory","create","config","deps","method","path","inputSchema","outputSchema","handler","defineFragment","defineRoute","createFragment","FragnoPublicClientConfig","FragnoPublicConfig","z","createClientBuilder","chatRouteFactory","computed","ChatnoServerConfig","openaiApiKey","model","systemPrompt","healthRoute","method","path","outputSchema","object","status","literal","handler","simpleStreamRoute","array","message","string","DEFAULT_SYSTEM_PROMPT","chatnoDefinition","withDependencies","withServices","routes","const","createChatno","chatnoConfig","fragnoConfig","config","createChatnoClient","cb","chatStream","createMutator","aggregatedMessage","mutatorStore","data","filter","item","type","map","delta","join","sendMessage","mutate","body","messages","id","crypto","randomUUID","role","content","useSendMessage","createStore","response","responseLoading","loading","FragnoRouteConfig"],"sources":["../../src/server/chatno-api.ts","../../src/index.ts"],"sourcesContent":["import OpenAI from \"openai\";\nimport { z } from \"zod\";\nimport type {\n EasyInputMessage,\n ResponseFunctionToolCall,\n ResponseInputItem,\n} from \"openai/resources/responses/responses.mjs\";\nimport { defineRoute, defineRoutes } from \"@fragno-dev/core\";\n\nexport const ChatMessageSchema = z.object({\n type: z.literal(\"chat\"),\n id: z.string(),\n role: z.enum([\"user\", \"assistant\"]),\n content: z.string(),\n});\n\nexport const FunctionCallMessageSchema = z.object({\n type: z.literal(\"functionCall\"),\n id: z.string(),\n functionCallId: z.string(),\n name: z.string(),\n arguments: z.string(),\n});\n\nexport const FunctionCallOutputMessageSchema = z.object({\n type: z.literal(\"functionCallOutput\"),\n id: z.string(),\n functionCallId: z.string(),\n output: z.string(),\n status: z.enum([\"inProgress\", \"completed\", \"incomplete\"]),\n});\n\nexport const InputMessageSchema = z.discriminatedUnion(\"type\", [\n ChatMessageSchema,\n FunctionCallMessageSchema,\n FunctionCallOutputMessageSchema,\n]);\n\n// Request schema with unified messages array\nexport const ChatStreamRequestSchema = z.object({\n messages: z.array(InputMessageSchema),\n});\n\nexport const ResponseTextDeltaEventSchema = z.object({\n type: z.literal(\"response.output_text.delta\"),\n content_index: z.number(),\n delta: z.string(),\n item_id: z.string(),\n output_index: z.number(),\n sequence_number: z.number(),\n});\n\nexport const ResponseTextDoneEventSchema = z.object({\n type: z.literal(\"response.output_text.done\"),\n content_index: z.number(),\n item_id: z.string(),\n output_index: z.number(),\n sequence_number: z.number(),\n text: z.string(),\n});\n\nexport const ResponseEventSchema = z.discriminatedUnion(\"type\", [\n ResponseTextDeltaEventSchema,\n ResponseTextDoneEventSchema,\n]);\n\ntype ChatRouteConfig = {\n model: \"gpt-5-mini\" | \"4o-mini\";\n systemPrompt: string;\n};\n\ntype ChatRouteDeps = {\n openaiClient: OpenAI;\n};\n\nexport const chatRouteFactory = defineRoutes<ChatRouteConfig, ChatRouteDeps>().create(\n ({ config, deps }) => {\n const { openaiClient } = deps;\n const { model, systemPrompt } = config;\n\n return [\n defineRoute({\n method: \"POST\",\n path: \"/chat/stream\",\n inputSchema: ChatStreamRequestSchema,\n outputSchema: z.array(ResponseEventSchema),\n handler: async ({ input }, { jsonStream }) => {\n const { messages } = await input.valid();\n\n const openAIMessages: ResponseInputItem[] = messages.map((message) => {\n if (message.type === \"chat\") {\n return {\n role: message.role,\n content: message.content,\n } satisfies EasyInputMessage;\n }\n\n if (message.type === \"functionCall\") {\n return {\n type: \"function_call\",\n id: message.id,\n call_id: message.functionCallId,\n name: message.name,\n arguments: message.arguments,\n } satisfies ResponseFunctionToolCall;\n }\n\n if (message.type === \"functionCallOutput\") {\n return {\n type: \"function_call_output\",\n call_id: message.functionCallId,\n output: message.output,\n status:\n message.status === \"inProgress\"\n ? \"in_progress\"\n : message.status === \"completed\"\n ? \"completed\"\n : \"incomplete\",\n } satisfies ResponseInputItem.FunctionCallOutput;\n }\n\n throw new Error(\"unreachable\");\n });\n\n try {\n const eventStream = await openaiClient.responses.create({\n model,\n input: [\n {\n role: \"system\",\n content: systemPrompt,\n },\n ...openAIMessages,\n ],\n // tools: [],\n // tool_choice: \"auto\",\n stream: true,\n });\n\n return jsonStream(async (stream) => {\n for await (const event of eventStream) {\n if (\n event.type !== \"response.output_text.delta\" &&\n event.type !== \"response.output_text.done\"\n ) {\n continue;\n }\n\n await stream.write(event);\n }\n });\n } catch (error) {\n console.error(\"OpenAI API error:\", error);\n throw error;\n }\n },\n }),\n ];\n },\n);\n","import {\n defineFragment,\n defineRoute,\n createFragment,\n type FragnoPublicClientConfig,\n type FragnoPublicConfig,\n} from \"@fragno-dev/core\";\nimport OpenAI from \"openai\";\nimport { z } from \"zod\";\nimport { createClientBuilder } from \"@fragno-dev/core/client\";\nimport { chatRouteFactory } from \"./server/chatno-api\";\nimport { computed } from \"nanostores\";\n\nexport interface ChatnoServerConfig {\n openaiApiKey: string;\n model?: \"gpt-5-mini\" | \"4o-mini\" | \"gpt-5-nano\";\n systemPrompt?: string;\n}\n\nconst healthRoute = defineRoute({\n method: \"GET\",\n path: \"/health\",\n outputSchema: z.object({\n status: z.literal(\"ok\"),\n }),\n handler: async (_ctx, { json }) => {\n return json({ status: \"ok\" });\n },\n});\n\nconst simpleStreamRoute = defineRoute({\n method: \"GET\",\n path: \"/simple-stream\",\n outputSchema: z.array(\n z.object({\n message: z.string(),\n }),\n ),\n handler: async (_ctx, { jsonStream }) => {\n return jsonStream(async (stream) => {\n for (let i = 0; i < 10; i++) {\n await stream.sleep(500);\n await stream.write({ message: `Item ${i + 1}` });\n }\n });\n },\n});\n\nconst DEFAULT_SYSTEM_PROMPT = `You are an AI assistant integrated into a dashboard.`;\n\nconst chatnoDefinition = defineFragment<ChatnoServerConfig>(\"chatno\")\n .withDependencies((config: ChatnoServerConfig) => {\n return {\n openaiClient: new OpenAI({\n apiKey: config.openaiApiKey,\n }),\n };\n })\n .withServices((_cfg, deps) => {\n return {\n getOpenAIURL: () => deps.openaiClient.baseURL,\n };\n });\n\nconst routes = [chatRouteFactory, healthRoute, simpleStreamRoute] as const;\n\n// Server-side factory\nexport function createChatno(\n chatnoConfig: ChatnoServerConfig,\n fragnoConfig: FragnoPublicConfig = {},\n) {\n const config = {\n model: chatnoConfig.model ?? \"gpt-5-nano\",\n systemPrompt: chatnoConfig.systemPrompt ?? DEFAULT_SYSTEM_PROMPT,\n };\n\n return createFragment(chatnoDefinition, { ...chatnoConfig, ...config }, routes, fragnoConfig);\n}\n\n// Client-side factory\nexport function createChatnoClient(fragnoConfig: FragnoPublicClientConfig = {}) {\n const cb = createClientBuilder(chatnoDefinition, fragnoConfig, routes);\n\n const chatStream = cb.createMutator(\"POST\", \"/chat/stream\");\n\n const aggregatedMessage = computed(chatStream.mutatorStore, ({ data }) => {\n return (data ?? [])\n .filter((item) => item.type === \"response.output_text.delta\")\n .map((item) => item.delta)\n .join(\"\");\n });\n\n function sendMessage(message: string) {\n chatStream.mutatorStore.mutate({\n body: {\n messages: [{ type: \"chat\", id: crypto.randomUUID(), role: \"user\", content: message }],\n },\n });\n }\n\n return {\n useSendMessage: cb.createStore({\n response: aggregatedMessage,\n responseLoading: computed(chatStream.mutatorStore, ({ loading }) => loading),\n sendMessage,\n }),\n };\n}\n\nexport type { FragnoRouteConfig } from \"@fragno-dev/core/api\";\n"],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as _fragno_dev_core0 from "@fragno-dev/core";
|
|
2
|
+
import { FragnoPublicClientConfig, FragnoPublicConfig } from "@fragno-dev/core";
|
|
3
|
+
import OpenAI from "openai";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import * as _fragno_dev_core_client0 from "@fragno-dev/core/client";
|
|
6
|
+
import * as nanostores0 from "nanostores";
|
|
7
|
+
import { FragnoRouteConfig } from "@fragno-dev/core/api";
|
|
8
|
+
|
|
9
|
+
//#region src/index.d.ts
|
|
10
|
+
interface ChatnoServerConfig {
|
|
11
|
+
openaiApiKey: string;
|
|
12
|
+
model?: "gpt-5-mini" | "4o-mini" | "gpt-5-nano";
|
|
13
|
+
systemPrompt?: string;
|
|
14
|
+
}
|
|
15
|
+
declare function createChatno(chatnoConfig: ChatnoServerConfig, fragnoConfig?: FragnoPublicConfig): _fragno_dev_core0.FragnoInstantiatedFragment<[_fragno_dev_core0.FragnoRouteConfig<"POST", "/chat/stream", z.ZodObject<{
|
|
16
|
+
messages: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
17
|
+
type: z.ZodLiteral<"chat">;
|
|
18
|
+
id: z.ZodString;
|
|
19
|
+
role: z.ZodEnum<{
|
|
20
|
+
user: "user";
|
|
21
|
+
assistant: "assistant";
|
|
22
|
+
}>;
|
|
23
|
+
content: z.ZodString;
|
|
24
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
25
|
+
type: z.ZodLiteral<"functionCall">;
|
|
26
|
+
id: z.ZodString;
|
|
27
|
+
functionCallId: z.ZodString;
|
|
28
|
+
name: z.ZodString;
|
|
29
|
+
arguments: z.ZodString;
|
|
30
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
31
|
+
type: z.ZodLiteral<"functionCallOutput">;
|
|
32
|
+
id: z.ZodString;
|
|
33
|
+
functionCallId: z.ZodString;
|
|
34
|
+
output: z.ZodString;
|
|
35
|
+
status: z.ZodEnum<{
|
|
36
|
+
inProgress: "inProgress";
|
|
37
|
+
completed: "completed";
|
|
38
|
+
incomplete: "incomplete";
|
|
39
|
+
}>;
|
|
40
|
+
}, z.core.$strip>], "type">>;
|
|
41
|
+
}, z.core.$strip>, z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
42
|
+
type: z.ZodLiteral<"response.output_text.delta">;
|
|
43
|
+
content_index: z.ZodNumber;
|
|
44
|
+
delta: z.ZodString;
|
|
45
|
+
item_id: z.ZodString;
|
|
46
|
+
output_index: z.ZodNumber;
|
|
47
|
+
sequence_number: z.ZodNumber;
|
|
48
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
49
|
+
type: z.ZodLiteral<"response.output_text.done">;
|
|
50
|
+
content_index: z.ZodNumber;
|
|
51
|
+
item_id: z.ZodString;
|
|
52
|
+
output_index: z.ZodNumber;
|
|
53
|
+
sequence_number: z.ZodNumber;
|
|
54
|
+
text: z.ZodString;
|
|
55
|
+
}, z.core.$strip>], "type">>, string, string>, _fragno_dev_core0.FragnoRouteConfig<"GET", "/health", undefined, z.ZodObject<{
|
|
56
|
+
status: z.ZodLiteral<"ok">;
|
|
57
|
+
}, z.core.$strip>, string, string>, _fragno_dev_core0.FragnoRouteConfig<"GET", "/simple-stream", undefined, z.ZodArray<z.ZodObject<{
|
|
58
|
+
message: z.ZodString;
|
|
59
|
+
}, z.core.$strip>>, string, string>], {
|
|
60
|
+
openaiClient: OpenAI;
|
|
61
|
+
}, {
|
|
62
|
+
getOpenAIURL: () => string;
|
|
63
|
+
}>;
|
|
64
|
+
declare function createChatnoClient(fragnoConfig?: FragnoPublicClientConfig): {
|
|
65
|
+
useSendMessage: _fragno_dev_core_client0.FragnoStoreData<{
|
|
66
|
+
readonly response: nanostores0.ReadableAtom<string>;
|
|
67
|
+
readonly responseLoading: nanostores0.ReadableAtom<boolean | undefined>;
|
|
68
|
+
readonly sendMessage: (message: string) => void;
|
|
69
|
+
}>;
|
|
70
|
+
};
|
|
71
|
+
//#endregion
|
|
72
|
+
export { ChatnoServerConfig, type FragnoRouteConfig, createChatno, createChatnoClient };
|
|
73
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;UAaiB,kBAAA;;;;;iBAsDD,YAAA,eACA,mCACA,uCAAuB,8CAAA,0CAAA,CAAA,CAAA;EAxDtB,QAAA,YAAA,wBAAkB,CAAA,YAAA,CAAA;IAsDnB,IAAA,cAAY,CAAA,MAAA,CAAA;IAAA,EAAA,aAAA;IACZ,IAAA,WAAA,CAAA;MACA,IAAA,EAAA,MAAA;;;;;;;;;;;;;;;;;;;;;;;;OAAuB,aAAA;;;;;;;;;;;;;;;;;;;;iBAWvB,kBAAA,gBAAiC;;uBAA6B,WAAA,CAAA"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { chatRouteFactory } from "./server/chatno-api.js";
|
|
2
|
+
import { createFragment, defineFragment, defineRoute } from "@fragno-dev/core";
|
|
3
|
+
import OpenAI from "openai";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { createClientBuilder } from "@fragno-dev/core/client";
|
|
6
|
+
import { computed } from "nanostores";
|
|
7
|
+
|
|
8
|
+
//#region src/index.ts
|
|
9
|
+
const healthRoute = defineRoute({
|
|
10
|
+
method: "GET",
|
|
11
|
+
path: "/health",
|
|
12
|
+
outputSchema: z.object({ status: z.literal("ok") }),
|
|
13
|
+
handler: async (_ctx, { json }) => {
|
|
14
|
+
return json({ status: "ok" });
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
const simpleStreamRoute = defineRoute({
|
|
18
|
+
method: "GET",
|
|
19
|
+
path: "/simple-stream",
|
|
20
|
+
outputSchema: z.array(z.object({ message: z.string() })),
|
|
21
|
+
handler: async (_ctx, { jsonStream }) => {
|
|
22
|
+
return jsonStream(async (stream) => {
|
|
23
|
+
for (let i = 0; i < 10; i++) {
|
|
24
|
+
await stream.sleep(500);
|
|
25
|
+
await stream.write({ message: `Item ${i + 1}` });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
const DEFAULT_SYSTEM_PROMPT = `You are an AI assistant integrated into a dashboard.`;
|
|
31
|
+
const chatnoDefinition = defineFragment("chatno").withDependencies((config) => {
|
|
32
|
+
return { openaiClient: new OpenAI({ apiKey: config.openaiApiKey }) };
|
|
33
|
+
}).withServices((_cfg, deps) => {
|
|
34
|
+
return { getOpenAIURL: () => deps.openaiClient.baseURL };
|
|
35
|
+
});
|
|
36
|
+
const routes = [
|
|
37
|
+
chatRouteFactory,
|
|
38
|
+
healthRoute,
|
|
39
|
+
simpleStreamRoute
|
|
40
|
+
];
|
|
41
|
+
function createChatno(chatnoConfig, fragnoConfig = {}) {
|
|
42
|
+
const config = {
|
|
43
|
+
model: chatnoConfig.model ?? "gpt-5-nano",
|
|
44
|
+
systemPrompt: chatnoConfig.systemPrompt ?? DEFAULT_SYSTEM_PROMPT
|
|
45
|
+
};
|
|
46
|
+
return createFragment(chatnoDefinition, {
|
|
47
|
+
...chatnoConfig,
|
|
48
|
+
...config
|
|
49
|
+
}, routes, fragnoConfig);
|
|
50
|
+
}
|
|
51
|
+
function createChatnoClient(fragnoConfig = {}) {
|
|
52
|
+
const cb = createClientBuilder(chatnoDefinition, fragnoConfig, routes);
|
|
53
|
+
const chatStream = cb.createMutator("POST", "/chat/stream");
|
|
54
|
+
const aggregatedMessage = computed(chatStream.mutatorStore, ({ data }) => {
|
|
55
|
+
return (data ?? []).filter((item) => item.type === "response.output_text.delta").map((item) => item.delta).join("");
|
|
56
|
+
});
|
|
57
|
+
function sendMessage(message) {
|
|
58
|
+
chatStream.mutatorStore.mutate({ body: { messages: [{
|
|
59
|
+
type: "chat",
|
|
60
|
+
id: crypto.randomUUID(),
|
|
61
|
+
role: "user",
|
|
62
|
+
content: message
|
|
63
|
+
}] } });
|
|
64
|
+
}
|
|
65
|
+
return { useSendMessage: cb.createStore({
|
|
66
|
+
response: aggregatedMessage,
|
|
67
|
+
responseLoading: computed(chatStream.mutatorStore, ({ loading }) => loading),
|
|
68
|
+
sendMessage
|
|
69
|
+
}) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
export { createChatno, createChatnoClient };
|
|
74
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/index.ts"],"sourcesContent":["import {\n defineFragment,\n defineRoute,\n createFragment,\n type FragnoPublicClientConfig,\n type FragnoPublicConfig,\n} from \"@fragno-dev/core\";\nimport OpenAI from \"openai\";\nimport { z } from \"zod\";\nimport { createClientBuilder } from \"@fragno-dev/core/client\";\nimport { chatRouteFactory } from \"./server/chatno-api\";\nimport { computed } from \"nanostores\";\n\nexport interface ChatnoServerConfig {\n openaiApiKey: string;\n model?: \"gpt-5-mini\" | \"4o-mini\" | \"gpt-5-nano\";\n systemPrompt?: string;\n}\n\nconst healthRoute = defineRoute({\n method: \"GET\",\n path: \"/health\",\n outputSchema: z.object({\n status: z.literal(\"ok\"),\n }),\n handler: async (_ctx, { json }) => {\n return json({ status: \"ok\" });\n },\n});\n\nconst simpleStreamRoute = defineRoute({\n method: \"GET\",\n path: \"/simple-stream\",\n outputSchema: z.array(\n z.object({\n message: z.string(),\n }),\n ),\n handler: async (_ctx, { jsonStream }) => {\n return jsonStream(async (stream) => {\n for (let i = 0; i < 10; i++) {\n await stream.sleep(500);\n await stream.write({ message: `Item ${i + 1}` });\n }\n });\n },\n});\n\nconst DEFAULT_SYSTEM_PROMPT = `You are an AI assistant integrated into a dashboard.`;\n\nconst chatnoDefinition = defineFragment<ChatnoServerConfig>(\"chatno\")\n .withDependencies((config: ChatnoServerConfig) => {\n return {\n openaiClient: new OpenAI({\n apiKey: config.openaiApiKey,\n }),\n };\n })\n .withServices((_cfg, deps) => {\n return {\n getOpenAIURL: () => deps.openaiClient.baseURL,\n };\n });\n\nconst routes = [chatRouteFactory, healthRoute, simpleStreamRoute] as const;\n\n// Server-side factory\nexport function createChatno(\n chatnoConfig: ChatnoServerConfig,\n fragnoConfig: FragnoPublicConfig = {},\n) {\n const config = {\n model: chatnoConfig.model ?? \"gpt-5-nano\",\n systemPrompt: chatnoConfig.systemPrompt ?? DEFAULT_SYSTEM_PROMPT,\n };\n\n return createFragment(chatnoDefinition, { ...chatnoConfig, ...config }, routes, fragnoConfig);\n}\n\n// Client-side factory\nexport function createChatnoClient(fragnoConfig: FragnoPublicClientConfig = {}) {\n const cb = createClientBuilder(chatnoDefinition, fragnoConfig, routes);\n\n const chatStream = cb.createMutator(\"POST\", \"/chat/stream\");\n\n const aggregatedMessage = computed(chatStream.mutatorStore, ({ data }) => {\n return (data ?? [])\n .filter((item) => item.type === \"response.output_text.delta\")\n .map((item) => item.delta)\n .join(\"\");\n });\n\n function sendMessage(message: string) {\n chatStream.mutatorStore.mutate({\n body: {\n messages: [{ type: \"chat\", id: crypto.randomUUID(), role: \"user\", content: message }],\n },\n });\n }\n\n return {\n useSendMessage: cb.createStore({\n response: aggregatedMessage,\n responseLoading: computed(chatStream.mutatorStore, ({ loading }) => loading),\n sendMessage,\n }),\n };\n}\n\nexport type { FragnoRouteConfig } from \"@fragno-dev/core/api\";\n"],"mappings":";;;;;;;;AAmBA,MAAM,cAAc,YAAY;CAC9B,QAAQ;CACR,MAAM;CACN,cAAc,EAAE,OAAO,EACrB,QAAQ,EAAE,QAAQ,KAAK,EACxB,CAAC;CACF,SAAS,OAAO,MAAM,EAAE,WAAW;AACjC,SAAO,KAAK,EAAE,QAAQ,MAAM,CAAC;;CAEhC,CAAC;AAEF,MAAM,oBAAoB,YAAY;CACpC,QAAQ;CACR,MAAM;CACN,cAAc,EAAE,MACd,EAAE,OAAO,EACP,SAAS,EAAE,QAAQ,EACpB,CAAC,CACH;CACD,SAAS,OAAO,MAAM,EAAE,iBAAiB;AACvC,SAAO,WAAW,OAAO,WAAW;AAClC,QAAK,IAAI,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,UAAM,OAAO,MAAM,IAAI;AACvB,UAAM,OAAO,MAAM,EAAE,SAAS,QAAQ,IAAI,KAAK,CAAC;;IAElD;;CAEL,CAAC;AAEF,MAAM,wBAAwB;AAE9B,MAAM,mBAAmB,eAAmC,SAAS,CAClE,kBAAkB,WAA+B;AAChD,QAAO,EACL,cAAc,IAAI,OAAO,EACvB,QAAQ,OAAO,cAChB,CAAC,EACH;EACD,CACD,cAAc,MAAM,SAAS;AAC5B,QAAO,EACL,oBAAoB,KAAK,aAAa,SACvC;EACD;AAEJ,MAAM,SAAS;CAAC;CAAkB;CAAa;CAAkB;AAGjE,SAAgB,aACd,cACA,eAAmC,EAAE,EACrC;CACA,MAAM,SAAS;EACb,OAAO,aAAa,SAAS;EAC7B,cAAc,aAAa,gBAAgB;EAC5C;AAED,QAAO,eAAe,kBAAkB;EAAE,GAAG;EAAc,GAAG;EAAQ,EAAE,QAAQ,aAAa;;AAI/F,SAAgB,mBAAmB,eAAyC,EAAE,EAAE;CAC9E,MAAM,KAAK,oBAAoB,kBAAkB,cAAc,OAAO;CAEtE,MAAM,aAAa,GAAG,cAAc,QAAQ,eAAe;CAE3D,MAAM,oBAAoB,SAAS,WAAW,eAAe,EAAE,WAAW;AACxE,UAAQ,QAAQ,EAAE,EACf,QAAQ,SAAS,KAAK,SAAS,6BAA6B,CAC5D,KAAK,SAAS,KAAK,MAAM,CACzB,KAAK,GAAG;GACX;CAEF,SAAS,YAAY,SAAiB;AACpC,aAAW,aAAa,OAAO,EAC7B,MAAM,EACJ,UAAU,CAAC;GAAE,MAAM;GAAQ,IAAI,OAAO,YAAY;GAAE,MAAM;GAAQ,SAAS;GAAS,CAAC,EACtF,EACF,CAAC;;AAGJ,QAAO,EACL,gBAAgB,GAAG,YAAY;EAC7B,UAAU;EACV,iBAAiB,SAAS,WAAW,eAAe,EAAE,cAAc,QAAQ;EAC5E;EACD,CAAC,EACH"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { defineRoute, defineRoutes } from "@fragno-dev/core";
|
|
2
|
+
import OpenAI from "openai";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
//#region src/server/chatno-api.ts
|
|
6
|
+
const ChatMessageSchema = z.object({
|
|
7
|
+
type: z.literal("chat"),
|
|
8
|
+
id: z.string(),
|
|
9
|
+
role: z.enum(["user", "assistant"]),
|
|
10
|
+
content: z.string()
|
|
11
|
+
});
|
|
12
|
+
const FunctionCallMessageSchema = z.object({
|
|
13
|
+
type: z.literal("functionCall"),
|
|
14
|
+
id: z.string(),
|
|
15
|
+
functionCallId: z.string(),
|
|
16
|
+
name: z.string(),
|
|
17
|
+
arguments: z.string()
|
|
18
|
+
});
|
|
19
|
+
const FunctionCallOutputMessageSchema = z.object({
|
|
20
|
+
type: z.literal("functionCallOutput"),
|
|
21
|
+
id: z.string(),
|
|
22
|
+
functionCallId: z.string(),
|
|
23
|
+
output: z.string(),
|
|
24
|
+
status: z.enum([
|
|
25
|
+
"inProgress",
|
|
26
|
+
"completed",
|
|
27
|
+
"incomplete"
|
|
28
|
+
])
|
|
29
|
+
});
|
|
30
|
+
const InputMessageSchema = z.discriminatedUnion("type", [
|
|
31
|
+
ChatMessageSchema,
|
|
32
|
+
FunctionCallMessageSchema,
|
|
33
|
+
FunctionCallOutputMessageSchema
|
|
34
|
+
]);
|
|
35
|
+
const ChatStreamRequestSchema = z.object({ messages: z.array(InputMessageSchema) });
|
|
36
|
+
const ResponseTextDeltaEventSchema = z.object({
|
|
37
|
+
type: z.literal("response.output_text.delta"),
|
|
38
|
+
content_index: z.number(),
|
|
39
|
+
delta: z.string(),
|
|
40
|
+
item_id: z.string(),
|
|
41
|
+
output_index: z.number(),
|
|
42
|
+
sequence_number: z.number()
|
|
43
|
+
});
|
|
44
|
+
const ResponseTextDoneEventSchema = z.object({
|
|
45
|
+
type: z.literal("response.output_text.done"),
|
|
46
|
+
content_index: z.number(),
|
|
47
|
+
item_id: z.string(),
|
|
48
|
+
output_index: z.number(),
|
|
49
|
+
sequence_number: z.number(),
|
|
50
|
+
text: z.string()
|
|
51
|
+
});
|
|
52
|
+
const ResponseEventSchema = z.discriminatedUnion("type", [ResponseTextDeltaEventSchema, ResponseTextDoneEventSchema]);
|
|
53
|
+
const chatRouteFactory = defineRoutes().create(({ config, deps }) => {
|
|
54
|
+
const { openaiClient } = deps;
|
|
55
|
+
const { model, systemPrompt } = config;
|
|
56
|
+
return [defineRoute({
|
|
57
|
+
method: "POST",
|
|
58
|
+
path: "/chat/stream",
|
|
59
|
+
inputSchema: ChatStreamRequestSchema,
|
|
60
|
+
outputSchema: z.array(ResponseEventSchema),
|
|
61
|
+
handler: async ({ input }, { jsonStream }) => {
|
|
62
|
+
const { messages } = await input.valid();
|
|
63
|
+
const openAIMessages = messages.map((message) => {
|
|
64
|
+
if (message.type === "chat") return {
|
|
65
|
+
role: message.role,
|
|
66
|
+
content: message.content
|
|
67
|
+
};
|
|
68
|
+
if (message.type === "functionCall") return {
|
|
69
|
+
type: "function_call",
|
|
70
|
+
id: message.id,
|
|
71
|
+
call_id: message.functionCallId,
|
|
72
|
+
name: message.name,
|
|
73
|
+
arguments: message.arguments
|
|
74
|
+
};
|
|
75
|
+
if (message.type === "functionCallOutput") return {
|
|
76
|
+
type: "function_call_output",
|
|
77
|
+
call_id: message.functionCallId,
|
|
78
|
+
output: message.output,
|
|
79
|
+
status: message.status === "inProgress" ? "in_progress" : message.status === "completed" ? "completed" : "incomplete"
|
|
80
|
+
};
|
|
81
|
+
throw new Error("unreachable");
|
|
82
|
+
});
|
|
83
|
+
try {
|
|
84
|
+
const eventStream = await openaiClient.responses.create({
|
|
85
|
+
model,
|
|
86
|
+
input: [{
|
|
87
|
+
role: "system",
|
|
88
|
+
content: systemPrompt
|
|
89
|
+
}, ...openAIMessages],
|
|
90
|
+
stream: true
|
|
91
|
+
});
|
|
92
|
+
return jsonStream(async (stream) => {
|
|
93
|
+
for await (const event of eventStream) {
|
|
94
|
+
if (event.type !== "response.output_text.delta" && event.type !== "response.output_text.done") continue;
|
|
95
|
+
await stream.write(event);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error("OpenAI API error:", error);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
})];
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
//#endregion
|
|
107
|
+
export { chatRouteFactory };
|
|
108
|
+
//# sourceMappingURL=chatno-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chatno-api.js","names":["openAIMessages: ResponseInputItem[]"],"sources":["../../../src/server/chatno-api.ts"],"sourcesContent":["import OpenAI from \"openai\";\nimport { z } from \"zod\";\nimport type {\n EasyInputMessage,\n ResponseFunctionToolCall,\n ResponseInputItem,\n} from \"openai/resources/responses/responses.mjs\";\nimport { defineRoute, defineRoutes } from \"@fragno-dev/core\";\n\nexport const ChatMessageSchema = z.object({\n type: z.literal(\"chat\"),\n id: z.string(),\n role: z.enum([\"user\", \"assistant\"]),\n content: z.string(),\n});\n\nexport const FunctionCallMessageSchema = z.object({\n type: z.literal(\"functionCall\"),\n id: z.string(),\n functionCallId: z.string(),\n name: z.string(),\n arguments: z.string(),\n});\n\nexport const FunctionCallOutputMessageSchema = z.object({\n type: z.literal(\"functionCallOutput\"),\n id: z.string(),\n functionCallId: z.string(),\n output: z.string(),\n status: z.enum([\"inProgress\", \"completed\", \"incomplete\"]),\n});\n\nexport const InputMessageSchema = z.discriminatedUnion(\"type\", [\n ChatMessageSchema,\n FunctionCallMessageSchema,\n FunctionCallOutputMessageSchema,\n]);\n\n// Request schema with unified messages array\nexport const ChatStreamRequestSchema = z.object({\n messages: z.array(InputMessageSchema),\n});\n\nexport const ResponseTextDeltaEventSchema = z.object({\n type: z.literal(\"response.output_text.delta\"),\n content_index: z.number(),\n delta: z.string(),\n item_id: z.string(),\n output_index: z.number(),\n sequence_number: z.number(),\n});\n\nexport const ResponseTextDoneEventSchema = z.object({\n type: z.literal(\"response.output_text.done\"),\n content_index: z.number(),\n item_id: z.string(),\n output_index: z.number(),\n sequence_number: z.number(),\n text: z.string(),\n});\n\nexport const ResponseEventSchema = z.discriminatedUnion(\"type\", [\n ResponseTextDeltaEventSchema,\n ResponseTextDoneEventSchema,\n]);\n\ntype ChatRouteConfig = {\n model: \"gpt-5-mini\" | \"4o-mini\";\n systemPrompt: string;\n};\n\ntype ChatRouteDeps = {\n openaiClient: OpenAI;\n};\n\nexport const chatRouteFactory = defineRoutes<ChatRouteConfig, ChatRouteDeps>().create(\n ({ config, deps }) => {\n const { openaiClient } = deps;\n const { model, systemPrompt } = config;\n\n return [\n defineRoute({\n method: \"POST\",\n path: \"/chat/stream\",\n inputSchema: ChatStreamRequestSchema,\n outputSchema: z.array(ResponseEventSchema),\n handler: async ({ input }, { jsonStream }) => {\n const { messages } = await input.valid();\n\n const openAIMessages: ResponseInputItem[] = messages.map((message) => {\n if (message.type === \"chat\") {\n return {\n role: message.role,\n content: message.content,\n } satisfies EasyInputMessage;\n }\n\n if (message.type === \"functionCall\") {\n return {\n type: \"function_call\",\n id: message.id,\n call_id: message.functionCallId,\n name: message.name,\n arguments: message.arguments,\n } satisfies ResponseFunctionToolCall;\n }\n\n if (message.type === \"functionCallOutput\") {\n return {\n type: \"function_call_output\",\n call_id: message.functionCallId,\n output: message.output,\n status:\n message.status === \"inProgress\"\n ? \"in_progress\"\n : message.status === \"completed\"\n ? \"completed\"\n : \"incomplete\",\n } satisfies ResponseInputItem.FunctionCallOutput;\n }\n\n throw new Error(\"unreachable\");\n });\n\n try {\n const eventStream = await openaiClient.responses.create({\n model,\n input: [\n {\n role: \"system\",\n content: systemPrompt,\n },\n ...openAIMessages,\n ],\n // tools: [],\n // tool_choice: \"auto\",\n stream: true,\n });\n\n return jsonStream(async (stream) => {\n for await (const event of eventStream) {\n if (\n event.type !== \"response.output_text.delta\" &&\n event.type !== \"response.output_text.done\"\n ) {\n continue;\n }\n\n await stream.write(event);\n }\n });\n } catch (error) {\n console.error(\"OpenAI API error:\", error);\n throw error;\n }\n },\n }),\n ];\n },\n);\n"],"mappings":";;;;;AASA,MAAa,oBAAoB,EAAE,OAAO;CACxC,MAAM,EAAE,QAAQ,OAAO;CACvB,IAAI,EAAE,QAAQ;CACd,MAAM,EAAE,KAAK,CAAC,QAAQ,YAAY,CAAC;CACnC,SAAS,EAAE,QAAQ;CACpB,CAAC;AAEF,MAAa,4BAA4B,EAAE,OAAO;CAChD,MAAM,EAAE,QAAQ,eAAe;CAC/B,IAAI,EAAE,QAAQ;CACd,gBAAgB,EAAE,QAAQ;CAC1B,MAAM,EAAE,QAAQ;CAChB,WAAW,EAAE,QAAQ;CACtB,CAAC;AAEF,MAAa,kCAAkC,EAAE,OAAO;CACtD,MAAM,EAAE,QAAQ,qBAAqB;CACrC,IAAI,EAAE,QAAQ;CACd,gBAAgB,EAAE,QAAQ;CAC1B,QAAQ,EAAE,QAAQ;CAClB,QAAQ,EAAE,KAAK;EAAC;EAAc;EAAa;EAAa,CAAC;CAC1D,CAAC;AAEF,MAAa,qBAAqB,EAAE,mBAAmB,QAAQ;CAC7D;CACA;CACA;CACD,CAAC;AAGF,MAAa,0BAA0B,EAAE,OAAO,EAC9C,UAAU,EAAE,MAAM,mBAAmB,EACtC,CAAC;AAEF,MAAa,+BAA+B,EAAE,OAAO;CACnD,MAAM,EAAE,QAAQ,6BAA6B;CAC7C,eAAe,EAAE,QAAQ;CACzB,OAAO,EAAE,QAAQ;CACjB,SAAS,EAAE,QAAQ;CACnB,cAAc,EAAE,QAAQ;CACxB,iBAAiB,EAAE,QAAQ;CAC5B,CAAC;AAEF,MAAa,8BAA8B,EAAE,OAAO;CAClD,MAAM,EAAE,QAAQ,4BAA4B;CAC5C,eAAe,EAAE,QAAQ;CACzB,SAAS,EAAE,QAAQ;CACnB,cAAc,EAAE,QAAQ;CACxB,iBAAiB,EAAE,QAAQ;CAC3B,MAAM,EAAE,QAAQ;CACjB,CAAC;AAEF,MAAa,sBAAsB,EAAE,mBAAmB,QAAQ,CAC9D,8BACA,4BACD,CAAC;AAWF,MAAa,mBAAmB,cAA8C,CAAC,QAC5E,EAAE,QAAQ,WAAW;CACpB,MAAM,EAAE,iBAAiB;CACzB,MAAM,EAAE,OAAO,iBAAiB;AAEhC,QAAO,CACL,YAAY;EACV,QAAQ;EACR,MAAM;EACN,aAAa;EACb,cAAc,EAAE,MAAM,oBAAoB;EAC1C,SAAS,OAAO,EAAE,SAAS,EAAE,iBAAiB;GAC5C,MAAM,EAAE,aAAa,MAAM,MAAM,OAAO;GAExC,MAAMA,iBAAsC,SAAS,KAAK,YAAY;AACpE,QAAI,QAAQ,SAAS,OACnB,QAAO;KACL,MAAM,QAAQ;KACd,SAAS,QAAQ;KAClB;AAGH,QAAI,QAAQ,SAAS,eACnB,QAAO;KACL,MAAM;KACN,IAAI,QAAQ;KACZ,SAAS,QAAQ;KACjB,MAAM,QAAQ;KACd,WAAW,QAAQ;KACpB;AAGH,QAAI,QAAQ,SAAS,qBACnB,QAAO;KACL,MAAM;KACN,SAAS,QAAQ;KACjB,QAAQ,QAAQ;KAChB,QACE,QAAQ,WAAW,eACf,gBACA,QAAQ,WAAW,cACjB,cACA;KACT;AAGH,UAAM,IAAI,MAAM,cAAc;KAC9B;AAEF,OAAI;IACF,MAAM,cAAc,MAAM,aAAa,UAAU,OAAO;KACtD;KACA,OAAO,CACL;MACE,MAAM;MACN,SAAS;MACV,EACD,GAAG,eACJ;KAGD,QAAQ;KACT,CAAC;AAEF,WAAO,WAAW,OAAO,WAAW;AAClC,gBAAW,MAAM,SAAS,aAAa;AACrC,UACE,MAAM,SAAS,gCACf,MAAM,SAAS,4BAEf;AAGF,YAAM,OAAO,MAAM,MAAM;;MAE3B;YACK,OAAO;AACd,YAAQ,MAAM,qBAAqB,MAAM;AACzC,UAAM;;;EAGX,CAAC,CACH;EAEJ"}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fragno-dev/chatno",
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"exports": {
|
|
5
|
+
".": {
|
|
6
|
+
"development": {
|
|
7
|
+
"browser": "./dist/browser/index.js",
|
|
8
|
+
"default": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"bun": "./src/index.ts",
|
|
11
|
+
"types": "./dist/browser/index.d.ts",
|
|
12
|
+
"browser": "./dist/browser/index.js",
|
|
13
|
+
"default": "./dist/node/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/node/index.js",
|
|
17
|
+
"module": "./dist/node/index.js",
|
|
18
|
+
"types": "./dist/node/index.d.ts",
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsdown",
|
|
21
|
+
"build:watch": "tsdown --watch",
|
|
22
|
+
"types:check": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@fragno-dev/core": "0.0.5",
|
|
27
|
+
"nanostores": "^1.0.1",
|
|
28
|
+
"openai": "^5.20.0",
|
|
29
|
+
"zod": "^4.0.5"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^20",
|
|
33
|
+
"@fragno-private/typescript-config": "0.0.1",
|
|
34
|
+
"@fragno-dev/unplugin-fragno": "0.0.1"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineFragment,
|
|
3
|
+
defineRoute,
|
|
4
|
+
createFragment,
|
|
5
|
+
type FragnoPublicClientConfig,
|
|
6
|
+
type FragnoPublicConfig,
|
|
7
|
+
} from "@fragno-dev/core";
|
|
8
|
+
import OpenAI from "openai";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { createClientBuilder } from "@fragno-dev/core/client";
|
|
11
|
+
import { chatRouteFactory } from "./server/chatno-api";
|
|
12
|
+
import { computed } from "nanostores";
|
|
13
|
+
|
|
14
|
+
export interface ChatnoServerConfig {
|
|
15
|
+
openaiApiKey: string;
|
|
16
|
+
model?: "gpt-5-mini" | "4o-mini" | "gpt-5-nano";
|
|
17
|
+
systemPrompt?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const healthRoute = defineRoute({
|
|
21
|
+
method: "GET",
|
|
22
|
+
path: "/health",
|
|
23
|
+
outputSchema: z.object({
|
|
24
|
+
status: z.literal("ok"),
|
|
25
|
+
}),
|
|
26
|
+
handler: async (_ctx, { json }) => {
|
|
27
|
+
return json({ status: "ok" });
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const simpleStreamRoute = defineRoute({
|
|
32
|
+
method: "GET",
|
|
33
|
+
path: "/simple-stream",
|
|
34
|
+
outputSchema: z.array(
|
|
35
|
+
z.object({
|
|
36
|
+
message: z.string(),
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
handler: async (_ctx, { jsonStream }) => {
|
|
40
|
+
return jsonStream(async (stream) => {
|
|
41
|
+
for (let i = 0; i < 10; i++) {
|
|
42
|
+
await stream.sleep(500);
|
|
43
|
+
await stream.write({ message: `Item ${i + 1}` });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const DEFAULT_SYSTEM_PROMPT = `You are an AI assistant integrated into a dashboard.`;
|
|
50
|
+
|
|
51
|
+
const chatnoDefinition = defineFragment<ChatnoServerConfig>("chatno")
|
|
52
|
+
.withDependencies((config: ChatnoServerConfig) => {
|
|
53
|
+
return {
|
|
54
|
+
openaiClient: new OpenAI({
|
|
55
|
+
apiKey: config.openaiApiKey,
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
})
|
|
59
|
+
.withServices((_cfg, deps) => {
|
|
60
|
+
return {
|
|
61
|
+
getOpenAIURL: () => deps.openaiClient.baseURL,
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const routes = [chatRouteFactory, healthRoute, simpleStreamRoute] as const;
|
|
66
|
+
|
|
67
|
+
// Server-side factory
|
|
68
|
+
export function createChatno(
|
|
69
|
+
chatnoConfig: ChatnoServerConfig,
|
|
70
|
+
fragnoConfig: FragnoPublicConfig = {},
|
|
71
|
+
) {
|
|
72
|
+
const config = {
|
|
73
|
+
model: chatnoConfig.model ?? "gpt-5-nano",
|
|
74
|
+
systemPrompt: chatnoConfig.systemPrompt ?? DEFAULT_SYSTEM_PROMPT,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return createFragment(chatnoDefinition, { ...chatnoConfig, ...config }, routes, fragnoConfig);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Client-side factory
|
|
81
|
+
export function createChatnoClient(fragnoConfig: FragnoPublicClientConfig = {}) {
|
|
82
|
+
const cb = createClientBuilder(chatnoDefinition, fragnoConfig, routes);
|
|
83
|
+
|
|
84
|
+
const chatStream = cb.createMutator("POST", "/chat/stream");
|
|
85
|
+
|
|
86
|
+
const aggregatedMessage = computed(chatStream.mutatorStore, ({ data }) => {
|
|
87
|
+
return (data ?? [])
|
|
88
|
+
.filter((item) => item.type === "response.output_text.delta")
|
|
89
|
+
.map((item) => item.delta)
|
|
90
|
+
.join("");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
function sendMessage(message: string) {
|
|
94
|
+
chatStream.mutatorStore.mutate({
|
|
95
|
+
body: {
|
|
96
|
+
messages: [{ type: "chat", id: crypto.randomUUID(), role: "user", content: message }],
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
useSendMessage: cb.createStore({
|
|
103
|
+
response: aggregatedMessage,
|
|
104
|
+
responseLoading: computed(chatStream.mutatorStore, ({ loading }) => loading),
|
|
105
|
+
sendMessage,
|
|
106
|
+
}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type { FragnoRouteConfig } from "@fragno-dev/core/api";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface MessageService {
|
|
2
|
+
setData: (messageKey: string, message: string) => Promise<void>;
|
|
3
|
+
getData: (messageKey: string) => Promise<string | undefined>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const inMemoryMessageStore: Record<string, string> = {};
|
|
7
|
+
export const inMemoryMessageService: MessageService = {
|
|
8
|
+
setData: async (messageKey: string, message: string) => {
|
|
9
|
+
console.log("[InMemoryMessageService] setData", messageKey);
|
|
10
|
+
inMemoryMessageStore[messageKey] = message;
|
|
11
|
+
},
|
|
12
|
+
getData: async (messageKey: string) => {
|
|
13
|
+
console.log("[InMemoryMessageService] getData", messageKey);
|
|
14
|
+
return inMemoryMessageStore[messageKey];
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
inMemoryMessageService.setData("default", "Hello World");
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type {
|
|
4
|
+
EasyInputMessage,
|
|
5
|
+
ResponseFunctionToolCall,
|
|
6
|
+
ResponseInputItem,
|
|
7
|
+
} from "openai/resources/responses/responses.mjs";
|
|
8
|
+
import { defineRoute, defineRoutes } from "@fragno-dev/core";
|
|
9
|
+
|
|
10
|
+
export const ChatMessageSchema = z.object({
|
|
11
|
+
type: z.literal("chat"),
|
|
12
|
+
id: z.string(),
|
|
13
|
+
role: z.enum(["user", "assistant"]),
|
|
14
|
+
content: z.string(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const FunctionCallMessageSchema = z.object({
|
|
18
|
+
type: z.literal("functionCall"),
|
|
19
|
+
id: z.string(),
|
|
20
|
+
functionCallId: z.string(),
|
|
21
|
+
name: z.string(),
|
|
22
|
+
arguments: z.string(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const FunctionCallOutputMessageSchema = z.object({
|
|
26
|
+
type: z.literal("functionCallOutput"),
|
|
27
|
+
id: z.string(),
|
|
28
|
+
functionCallId: z.string(),
|
|
29
|
+
output: z.string(),
|
|
30
|
+
status: z.enum(["inProgress", "completed", "incomplete"]),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const InputMessageSchema = z.discriminatedUnion("type", [
|
|
34
|
+
ChatMessageSchema,
|
|
35
|
+
FunctionCallMessageSchema,
|
|
36
|
+
FunctionCallOutputMessageSchema,
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
// Request schema with unified messages array
|
|
40
|
+
export const ChatStreamRequestSchema = z.object({
|
|
41
|
+
messages: z.array(InputMessageSchema),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const ResponseTextDeltaEventSchema = z.object({
|
|
45
|
+
type: z.literal("response.output_text.delta"),
|
|
46
|
+
content_index: z.number(),
|
|
47
|
+
delta: z.string(),
|
|
48
|
+
item_id: z.string(),
|
|
49
|
+
output_index: z.number(),
|
|
50
|
+
sequence_number: z.number(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const ResponseTextDoneEventSchema = z.object({
|
|
54
|
+
type: z.literal("response.output_text.done"),
|
|
55
|
+
content_index: z.number(),
|
|
56
|
+
item_id: z.string(),
|
|
57
|
+
output_index: z.number(),
|
|
58
|
+
sequence_number: z.number(),
|
|
59
|
+
text: z.string(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const ResponseEventSchema = z.discriminatedUnion("type", [
|
|
63
|
+
ResponseTextDeltaEventSchema,
|
|
64
|
+
ResponseTextDoneEventSchema,
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
type ChatRouteConfig = {
|
|
68
|
+
model: "gpt-5-mini" | "4o-mini";
|
|
69
|
+
systemPrompt: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type ChatRouteDeps = {
|
|
73
|
+
openaiClient: OpenAI;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const chatRouteFactory = defineRoutes<ChatRouteConfig, ChatRouteDeps>().create(
|
|
77
|
+
({ config, deps }) => {
|
|
78
|
+
const { openaiClient } = deps;
|
|
79
|
+
const { model, systemPrompt } = config;
|
|
80
|
+
|
|
81
|
+
return [
|
|
82
|
+
defineRoute({
|
|
83
|
+
method: "POST",
|
|
84
|
+
path: "/chat/stream",
|
|
85
|
+
inputSchema: ChatStreamRequestSchema,
|
|
86
|
+
outputSchema: z.array(ResponseEventSchema),
|
|
87
|
+
handler: async ({ input }, { jsonStream }) => {
|
|
88
|
+
const { messages } = await input.valid();
|
|
89
|
+
|
|
90
|
+
const openAIMessages: ResponseInputItem[] = messages.map((message) => {
|
|
91
|
+
if (message.type === "chat") {
|
|
92
|
+
return {
|
|
93
|
+
role: message.role,
|
|
94
|
+
content: message.content,
|
|
95
|
+
} satisfies EasyInputMessage;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (message.type === "functionCall") {
|
|
99
|
+
return {
|
|
100
|
+
type: "function_call",
|
|
101
|
+
id: message.id,
|
|
102
|
+
call_id: message.functionCallId,
|
|
103
|
+
name: message.name,
|
|
104
|
+
arguments: message.arguments,
|
|
105
|
+
} satisfies ResponseFunctionToolCall;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (message.type === "functionCallOutput") {
|
|
109
|
+
return {
|
|
110
|
+
type: "function_call_output",
|
|
111
|
+
call_id: message.functionCallId,
|
|
112
|
+
output: message.output,
|
|
113
|
+
status:
|
|
114
|
+
message.status === "inProgress"
|
|
115
|
+
? "in_progress"
|
|
116
|
+
: message.status === "completed"
|
|
117
|
+
? "completed"
|
|
118
|
+
: "incomplete",
|
|
119
|
+
} satisfies ResponseInputItem.FunctionCallOutput;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
throw new Error("unreachable");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const eventStream = await openaiClient.responses.create({
|
|
127
|
+
model,
|
|
128
|
+
input: [
|
|
129
|
+
{
|
|
130
|
+
role: "system",
|
|
131
|
+
content: systemPrompt,
|
|
132
|
+
},
|
|
133
|
+
...openAIMessages,
|
|
134
|
+
],
|
|
135
|
+
// tools: [],
|
|
136
|
+
// tool_choice: "auto",
|
|
137
|
+
stream: true,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return jsonStream(async (stream) => {
|
|
141
|
+
for await (const event of eventStream) {
|
|
142
|
+
if (
|
|
143
|
+
event.type !== "response.output_text.delta" &&
|
|
144
|
+
event.type !== "response.output_text.done"
|
|
145
|
+
) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
await stream.write(event);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error("OpenAI API error:", error);
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
}),
|
|
158
|
+
];
|
|
159
|
+
},
|
|
160
|
+
);
|
package/tsconfig.json
ADDED
package/tsdown.config.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineConfig } from "tsdown";
|
|
2
|
+
import unpluginFragno from "@fragno-dev/unplugin-fragno/rollup";
|
|
3
|
+
|
|
4
|
+
export default defineConfig([
|
|
5
|
+
{
|
|
6
|
+
ignoreWatch: ["./dist"],
|
|
7
|
+
entry: "./src/index.ts",
|
|
8
|
+
dts: {
|
|
9
|
+
sourcemap: true,
|
|
10
|
+
},
|
|
11
|
+
platform: "browser",
|
|
12
|
+
outDir: "./dist/browser",
|
|
13
|
+
plugins: [unpluginFragno({ platform: "browser" })],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
ignoreWatch: ["./dist"],
|
|
17
|
+
entry: "./src/index.ts",
|
|
18
|
+
dts: {
|
|
19
|
+
sourcemap: true,
|
|
20
|
+
},
|
|
21
|
+
outDir: "./dist/node",
|
|
22
|
+
// This plugin can be omitted, because it doesn't do anything for platform "node".
|
|
23
|
+
plugins: [unpluginFragno({ platform: "node" })],
|
|
24
|
+
unbundle: true,
|
|
25
|
+
},
|
|
26
|
+
]);
|