session.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. import path from "path";
  2. import { App } from "../app/";
  3. import { Identifier } from "../id/id";
  4. import { LLM } from "../llm/llm";
  5. import { Storage } from "../storage/storage";
  6. import { Log } from "../util/log";
  7. import {
  8. convertToModelMessages,
  9. generateText,
  10. stepCountIs,
  11. streamText,
  12. type TextUIPart,
  13. type ToolInvocationUIPart,
  14. type UIDataTypes,
  15. type UIMessage,
  16. type UIMessagePart,
  17. } from "ai";
  18. import { z } from "zod";
  19. import * as tools from "../tool";
  20. import { Decimal } from "decimal.js";
  21. import PROMPT_ANTHROPIC from "./prompt/anthropic.txt";
  22. import PROMPT_TITLE from "./prompt/title.txt";
  23. import type { Tool } from "../tool/tool";
  24. import { Share } from "../share/share";
  25. export namespace Session {
  26. const log = Log.create({ service: "session" });
  27. export const Info = z.object({
  28. id: Identifier.schema("session"),
  29. shareID: z.string().optional(),
  30. title: z.string(),
  31. });
  32. export type Info = z.output<typeof Info>;
  33. export type Message = UIMessage<{
  34. assistant?: {
  35. modelID: string;
  36. providerID: string;
  37. cost: number;
  38. tokens: {
  39. input: number;
  40. output: number;
  41. reasoning: number;
  42. };
  43. };
  44. time: {
  45. created: number;
  46. completed?: number;
  47. };
  48. sessionID: string;
  49. tool: Record<string, Tool.Metadata>;
  50. }>;
  51. const state = App.state("session", () => {
  52. const sessions = new Map<string, Info>();
  53. const messages = new Map<string, Message[]>();
  54. return {
  55. sessions,
  56. messages,
  57. };
  58. });
  59. export async function create() {
  60. const result: Info = {
  61. id: Identifier.descending("session"),
  62. title: "New Session - " + new Date().toISOString(),
  63. };
  64. log.info("created", result);
  65. state().sessions.set(result.id, result);
  66. await Storage.writeJSON("session/info/" + result.id, result);
  67. return result;
  68. }
  69. export async function get(id: string) {
  70. const result = state().sessions.get(id);
  71. if (result) {
  72. return result;
  73. }
  74. const read = await Storage.readJSON<Info>("session/info/" + id);
  75. state().sessions.set(id, read);
  76. return read as Info;
  77. }
  78. export async function share(id: string) {
  79. const session = await get(id);
  80. if (session.shareID) return session.shareID;
  81. const shareID = await Share.create(id);
  82. if (!shareID) return;
  83. await update(id, (draft) => {
  84. draft.shareID = shareID;
  85. });
  86. return shareID as string;
  87. }
  88. export async function update(id: string, editor: (session: Info) => void) {
  89. const { sessions } = state();
  90. const session = await get(id);
  91. if (!session) return;
  92. editor(session);
  93. sessions.set(id, session);
  94. await Storage.writeJSON("session/info/" + id, session);
  95. return session;
  96. }
  97. export async function messages(sessionID: string) {
  98. const match = state().messages.get(sessionID);
  99. if (match) {
  100. return match;
  101. }
  102. const result = [] as Message[];
  103. const list = Storage.list("session/message/" + sessionID);
  104. for await (const p of list) {
  105. const read = await Storage.readJSON<Message>(p);
  106. result.push(read);
  107. }
  108. state().messages.set(sessionID, result);
  109. return result;
  110. }
  111. export async function* list() {
  112. for await (const item of Storage.list("session/info")) {
  113. const sessionID = path.basename(item, ".json");
  114. yield get(sessionID);
  115. }
  116. }
  117. const pending = new Map<string, AbortController>();
  118. export function abort(sessionID: string) {
  119. const controller = pending.get(sessionID);
  120. if (!controller) return false;
  121. controller.abort();
  122. pending.delete(sessionID);
  123. return true;
  124. }
  125. export async function chat(input: {
  126. sessionID: string;
  127. providerID: string;
  128. modelID: string;
  129. parts: UIMessagePart<UIDataTypes>[];
  130. }) {
  131. const l = log.clone().tag("session", input.sessionID);
  132. l.info("chatting");
  133. const model = await LLM.findModel(input.providerID, input.modelID);
  134. const msgs = await messages(input.sessionID);
  135. async function write(msg: Message) {
  136. return Storage.writeJSON(
  137. "session/message/" + input.sessionID + "/" + msg.id,
  138. msg,
  139. );
  140. }
  141. const app = await App.use();
  142. if (msgs.length === 0) {
  143. const system: Message = {
  144. id: Identifier.ascending("message"),
  145. role: "system",
  146. parts: [
  147. {
  148. type: "text",
  149. text: PROMPT_ANTHROPIC,
  150. },
  151. ],
  152. metadata: {
  153. sessionID: input.sessionID,
  154. time: {
  155. created: Date.now(),
  156. },
  157. tool: {},
  158. },
  159. };
  160. const contextFile = Bun.file(path.join(app.root, "CONTEXT.md"));
  161. if (await contextFile.exists()) {
  162. const context = await contextFile.text();
  163. system.parts.push({
  164. type: "text",
  165. text: context,
  166. });
  167. }
  168. msgs.push(system);
  169. state().messages.set(input.sessionID, msgs);
  170. generateText({
  171. messages: convertToModelMessages([
  172. {
  173. role: "system",
  174. parts: [
  175. {
  176. type: "text",
  177. text: PROMPT_TITLE,
  178. },
  179. ],
  180. },
  181. {
  182. role: "user",
  183. parts: input.parts,
  184. },
  185. ]),
  186. model: model.instance,
  187. }).then((result) => {
  188. return Session.update(input.sessionID, (draft) => {
  189. draft.title = result.text;
  190. });
  191. });
  192. await write(system);
  193. }
  194. const msg: Message = {
  195. role: "user",
  196. id: Identifier.ascending("message"),
  197. parts: input.parts,
  198. metadata: {
  199. time: {
  200. created: Date.now(),
  201. },
  202. sessionID: input.sessionID,
  203. tool: {},
  204. },
  205. };
  206. msgs.push(msg);
  207. await write(msg);
  208. const next: Message = {
  209. id: Identifier.ascending("message"),
  210. role: "assistant",
  211. parts: [],
  212. metadata: {
  213. assistant: {
  214. cost: 0,
  215. tokens: {
  216. input: 0,
  217. output: 0,
  218. reasoning: 0,
  219. },
  220. modelID: input.modelID,
  221. providerID: input.providerID,
  222. },
  223. time: {
  224. created: Date.now(),
  225. },
  226. sessionID: input.sessionID,
  227. tool: {},
  228. },
  229. };
  230. const controller = new AbortController();
  231. pending.set(input.sessionID, controller);
  232. const result = streamText({
  233. onStepFinish: async (step) => {
  234. const assistant = next.metadata!.assistant!;
  235. assistant.tokens.input = step.usage.inputTokens ?? 0;
  236. assistant.tokens.output = step.usage.outputTokens ?? 0;
  237. assistant.tokens.reasoning = step.usage.reasoningTokens ?? 0;
  238. assistant.cost = new Decimal(0)
  239. .add(new Decimal(assistant.tokens.input).mul(model.info.cost.input))
  240. .add(new Decimal(assistant.tokens.output).mul(model.info.cost.output))
  241. .toNumber();
  242. await write(next);
  243. },
  244. abortSignal: controller.signal,
  245. maxRetries: 6,
  246. stopWhen: stepCountIs(1000),
  247. messages: convertToModelMessages(msgs),
  248. temperature: 0,
  249. tools,
  250. model: model.instance,
  251. });
  252. msgs.push(next);
  253. let text: TextUIPart | undefined;
  254. const reader = result.toUIMessageStream().getReader();
  255. while (true) {
  256. const result = await reader.read().catch((e) => {
  257. if (e instanceof DOMException && e.name === "AbortError") {
  258. return;
  259. }
  260. throw e;
  261. });
  262. if (!result) break;
  263. const { done, value } = result;
  264. if (done) break;
  265. l.info("part", {
  266. type: value.type,
  267. });
  268. switch (value.type) {
  269. case "start":
  270. break;
  271. case "start-step":
  272. text = undefined;
  273. next.parts.push({
  274. type: "step-start",
  275. });
  276. break;
  277. case "text":
  278. if (!text) {
  279. text = value;
  280. next.parts.push(value);
  281. break;
  282. }
  283. text.text += value.text;
  284. break;
  285. case "tool-call":
  286. next.parts.push({
  287. type: "tool-invocation",
  288. toolInvocation: {
  289. state: "call",
  290. ...value,
  291. },
  292. });
  293. break;
  294. case "tool-result":
  295. const match = next.parts.find(
  296. (p) =>
  297. p.type === "tool-invocation" &&
  298. p.toolInvocation.toolCallId === value.toolCallId,
  299. ) as ToolInvocationUIPart | undefined;
  300. if (match) {
  301. const { output, metadata } = value.result as any;
  302. next.metadata!.tool[value.toolCallId] = metadata;
  303. match.toolInvocation = {
  304. ...match.toolInvocation,
  305. state: "result",
  306. result: output,
  307. };
  308. }
  309. break;
  310. case "finish":
  311. break;
  312. case "finish-step":
  313. break;
  314. case "error":
  315. log.error("error", value);
  316. break;
  317. default:
  318. l.info("unhandled", {
  319. type: value.type,
  320. });
  321. }
  322. await write(next);
  323. }
  324. pending.delete(input.sessionID);
  325. next.metadata!.time.completed = Date.now();
  326. await write(next);
  327. return next;
  328. }
  329. }