@xalia/agent 0.6.9 → 0.6.11

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.
Files changed (199) hide show
  1. package/README.md +11 -0
  2. package/dist/agent/src/agent/agent.js +77 -18
  3. package/dist/agent/src/agent/agentUtils.js +3 -2
  4. package/dist/agent/src/agent/documentSummarizer.js +126 -0
  5. package/dist/agent/src/agent/dummyLLM.js +25 -22
  6. package/dist/agent/src/agent/imageGenLLM.js +22 -19
  7. package/dist/agent/src/agent/llm.js +1 -1
  8. package/dist/agent/src/agent/openAILLM.js +15 -12
  9. package/dist/agent/src/agent/openAILLMStreaming.js +68 -37
  10. package/dist/agent/src/agent/repeatLLM.js +16 -7
  11. package/dist/agent/src/agent/tokenCounter.js +390 -0
  12. package/dist/agent/src/agent/tokenCounter.test.js +206 -0
  13. package/dist/agent/src/agent/toolSettings.js +17 -0
  14. package/dist/agent/src/agent/tools/calculatorTool.js +45 -0
  15. package/dist/agent/src/agent/tools/contentExtractors/pdfToText.js +55 -0
  16. package/dist/agent/src/agent/tools/datetimeTool.js +38 -0
  17. package/dist/agent/src/agent/tools/fileManager/fileManagerTool.js +156 -0
  18. package/dist/agent/src/agent/tools/fileManager/index.js +31 -0
  19. package/dist/agent/src/agent/tools/fileManager/memoryFileManager.js +102 -0
  20. package/dist/agent/src/{chat/data → agent/tools/fileManager}/mimeTypes.js +3 -1
  21. package/dist/agent/src/agent/tools/fileManager/prompt.js +33 -0
  22. package/dist/agent/src/{chat/data/dbSessionFileModels.js → agent/tools/fileManager/types.js} +7 -0
  23. package/dist/agent/src/agent/tools/index.js +64 -0
  24. package/dist/agent/src/agent/tools/openUrlTool.js +57 -0
  25. package/dist/agent/src/agent/tools/renderTool.js +89 -0
  26. package/dist/agent/src/agent/tools/utils.js +61 -0
  27. package/dist/agent/src/{chat/utils/search.js → agent/tools/webSearch.js} +1 -2
  28. package/dist/agent/src/agent/tools/webSearchTool.js +40 -0
  29. package/dist/agent/src/chat/client/chatClient.js +28 -0
  30. package/dist/agent/src/chat/client/index.js +4 -1
  31. package/dist/agent/src/chat/client/sessionClient.js +28 -2
  32. package/dist/agent/src/chat/constants.js +8 -0
  33. package/dist/agent/src/chat/data/dbSessionFiles.js +11 -6
  34. package/dist/agent/src/chat/protocol/messages.js +5 -0
  35. package/dist/agent/src/chat/server/chatContextManager.js +45 -25
  36. package/dist/agent/src/chat/server/conversation.js +3 -0
  37. package/dist/agent/src/chat/server/imageGeneratorTools.js +20 -8
  38. package/dist/agent/src/chat/server/openAIRouterLLM.js +0 -3
  39. package/dist/agent/src/chat/server/openSession.js +218 -55
  40. package/dist/agent/src/chat/server/promptRefiner.js +86 -0
  41. package/dist/agent/src/chat/server/server.js +5 -1
  42. package/dist/agent/src/chat/server/sessionFileManager.js +22 -221
  43. package/dist/agent/src/chat/server/sessionRegistry.js +87 -0
  44. package/dist/agent/src/chat/server/titleGenerator.js +112 -0
  45. package/dist/agent/src/chat/server/titleGenerator.test.js +113 -0
  46. package/dist/agent/src/chat/server/tools.js +63 -287
  47. package/dist/agent/src/chat/utils/approvalManager.js +6 -3
  48. package/dist/agent/src/chat/utils/multiAsyncQueue.js +3 -0
  49. package/dist/agent/src/test/agent.test.js +16 -17
  50. package/dist/agent/src/test/chatContextManager.test.js +15 -3
  51. package/dist/agent/src/test/dbMcpServerConfigs.test.js +4 -4
  52. package/dist/agent/src/test/dbSessionFiles.test.js +17 -17
  53. package/dist/agent/src/test/testTools.js +6 -1
  54. package/dist/agent/src/test/tools.test.js +27 -9
  55. package/dist/agent/src/tool/agentChat.js +5 -2
  56. package/dist/agent/src/tool/chatMain.js +34 -7
  57. package/dist/agent/src/tool/commandPrompt.js +2 -2
  58. package/dist/agent/src/tool/files.js +7 -8
  59. package/package.json +8 -2
  60. package/.env.development +0 -1
  61. package/.prettierrc.json +0 -11
  62. package/dist/agent/src/agent/tools.js +0 -44
  63. package/eslint.config.mjs +0 -38
  64. package/scripts/chat_server +0 -8
  65. package/scripts/git_message +0 -31
  66. package/scripts/git_wip +0 -21
  67. package/scripts/pr_message +0 -18
  68. package/scripts/pr_review +0 -16
  69. package/scripts/setup_chat +0 -90
  70. package/scripts/shutdown_chat_server +0 -42
  71. package/scripts/start_chat_server +0 -24
  72. package/scripts/sudomcp_import +0 -23
  73. package/scripts/test_chat +0 -308
  74. package/src/agent/agent.ts +0 -624
  75. package/src/agent/agentUtils.ts +0 -285
  76. package/src/agent/compressingContextManager.ts +0 -129
  77. package/src/agent/context.ts +0 -265
  78. package/src/agent/contextWithWorkspace.ts +0 -162
  79. package/src/agent/dummyLLM.ts +0 -126
  80. package/src/agent/iAgentEventHandler.ts +0 -64
  81. package/src/agent/imageGenLLM.ts +0 -97
  82. package/src/agent/imageGenerator.ts +0 -45
  83. package/src/agent/iplatform.ts +0 -18
  84. package/src/agent/llm.ts +0 -74
  85. package/src/agent/mcpServerManager.ts +0 -541
  86. package/src/agent/nullAgentEventHandler.ts +0 -26
  87. package/src/agent/nullPlatform.ts +0 -13
  88. package/src/agent/openAI.ts +0 -123
  89. package/src/agent/openAILLM.ts +0 -95
  90. package/src/agent/openAILLMStreaming.ts +0 -609
  91. package/src/agent/promptProvider.ts +0 -87
  92. package/src/agent/repeatLLM.ts +0 -50
  93. package/src/agent/sudoMcpServerManager.ts +0 -361
  94. package/src/agent/tokenAuth.ts +0 -50
  95. package/src/agent/tools.ts +0 -57
  96. package/src/chat/client/chatClient.ts +0 -922
  97. package/src/chat/client/connection.test.ts +0 -241
  98. package/src/chat/client/connection.ts +0 -286
  99. package/src/chat/client/constants.ts +0 -1
  100. package/src/chat/client/index.ts +0 -18
  101. package/src/chat/client/interfaces.ts +0 -34
  102. package/src/chat/client/sessionClient.ts +0 -537
  103. package/src/chat/client/sessionFiles.ts +0 -142
  104. package/src/chat/client/teamManager.ts +0 -29
  105. package/src/chat/data/apiKeyManager.ts +0 -76
  106. package/src/chat/data/dataModels.ts +0 -101
  107. package/src/chat/data/database.ts +0 -997
  108. package/src/chat/data/dbMcpServerConfigs.ts +0 -59
  109. package/src/chat/data/dbSessionFileModels.ts +0 -113
  110. package/src/chat/data/dbSessionFiles.ts +0 -99
  111. package/src/chat/data/dbSessionMessages.ts +0 -102
  112. package/src/chat/data/mimeTypes.ts +0 -58
  113. package/src/chat/protocol/connectionMessages.ts +0 -49
  114. package/src/chat/protocol/constants.ts +0 -55
  115. package/src/chat/protocol/errors.ts +0 -16
  116. package/src/chat/protocol/messages.ts +0 -846
  117. package/src/chat/server/README.md +0 -127
  118. package/src/chat/server/chatContextManager.ts +0 -639
  119. package/src/chat/server/connectionManager.test.ts +0 -246
  120. package/src/chat/server/connectionManager.ts +0 -506
  121. package/src/chat/server/conversation.ts +0 -316
  122. package/src/chat/server/errorUtils.ts +0 -28
  123. package/src/chat/server/imageGeneratorTools.ts +0 -160
  124. package/src/chat/server/openAIRouterLLM.ts +0 -171
  125. package/src/chat/server/openSession.ts +0 -1689
  126. package/src/chat/server/openSessionMessageSender.ts +0 -4
  127. package/src/chat/server/server.ts +0 -175
  128. package/src/chat/server/sessionFileManager.ts +0 -422
  129. package/src/chat/server/sessionRegistry.test.ts +0 -137
  130. package/src/chat/server/sessionRegistry.ts +0 -1425
  131. package/src/chat/server/test-utils/mockFactories.ts +0 -422
  132. package/src/chat/server/tools.ts +0 -397
  133. package/src/chat/utils/agentSessionMap.ts +0 -76
  134. package/src/chat/utils/approvalManager.ts +0 -183
  135. package/src/chat/utils/asyncLock.ts +0 -43
  136. package/src/chat/utils/asyncQueue.ts +0 -62
  137. package/src/chat/utils/htmlToText.ts +0 -61
  138. package/src/chat/utils/multiAsyncQueue.ts +0 -62
  139. package/src/chat/utils/responseAwaiter.ts +0 -181
  140. package/src/chat/utils/search.ts +0 -139
  141. package/src/chat/utils/userResolver.ts +0 -48
  142. package/src/chat/utils/websocket.ts +0 -16
  143. package/src/index.ts +0 -0
  144. package/src/test/agent.test.ts +0 -590
  145. package/src/test/approvalManager.test.ts +0 -141
  146. package/src/test/chatContextManager.test.ts +0 -527
  147. package/src/test/clientServerConnection.test.ts +0 -205
  148. package/src/test/compressingContextManager.test.ts +0 -77
  149. package/src/test/context.test.ts +0 -150
  150. package/src/test/contextTestTools.ts +0 -95
  151. package/src/test/conversation.test.ts +0 -109
  152. package/src/test/db.test.ts +0 -363
  153. package/src/test/dbMcpServerConfigs.test.ts +0 -112
  154. package/src/test/dbSessionFiles.test.ts +0 -258
  155. package/src/test/dbSessionMessages.test.ts +0 -85
  156. package/src/test/dbTestTools.ts +0 -157
  157. package/src/test/imageLoad.test.ts +0 -15
  158. package/src/test/mcpServerManager.test.ts +0 -114
  159. package/src/test/multiAsyncQueue.test.ts +0 -183
  160. package/src/test/openaiStreaming.test.ts +0 -177
  161. package/src/test/prompt.test.ts +0 -27
  162. package/src/test/promptProvider.test.ts +0 -33
  163. package/src/test/responseAwaiter.test.ts +0 -103
  164. package/src/test/sudoMcpServerManager.test.ts +0 -63
  165. package/src/test/testTools.ts +0 -171
  166. package/src/test/tools.test.ts +0 -39
  167. package/src/tool/agentChat.ts +0 -194
  168. package/src/tool/agentMain.ts +0 -180
  169. package/src/tool/chatMain.ts +0 -594
  170. package/src/tool/commandPrompt.ts +0 -264
  171. package/src/tool/files.ts +0 -84
  172. package/src/tool/main.ts +0 -25
  173. package/src/tool/nodePlatform.ts +0 -73
  174. package/src/tool/options.ts +0 -144
  175. package/src/tool/prompt.ts +0 -101
  176. package/test_data/background_test_profile.json +0 -6
  177. package/test_data/background_test_script.json +0 -11
  178. package/test_data/dummyllm_script_crash.json +0 -32
  179. package/test_data/dummyllm_script_image_gen.json +0 -19
  180. package/test_data/dummyllm_script_image_gen_fe.json +0 -29
  181. package/test_data/dummyllm_script_invoke_image_gen_tool.json +0 -37
  182. package/test_data/dummyllm_script_render_tool.json +0 -29
  183. package/test_data/dummyllm_script_simplecalc.json +0 -28
  184. package/test_data/dummyllm_script_test_auto_approve.json +0 -81
  185. package/test_data/dummyllm_script_test_simplecalc_addition.json +0 -29
  186. package/test_data/frog.png +0 -0
  187. package/test_data/frog.png.b64 +0 -1
  188. package/test_data/git_message_profile.json +0 -4
  189. package/test_data/git_wip_system.txt +0 -5
  190. package/test_data/image_gen_test_profile.json +0 -5
  191. package/test_data/pr_message_profile.json +0 -4
  192. package/test_data/pr_review_profile.json +0 -4
  193. package/test_data/prompt_simplecalc.txt +0 -1
  194. package/test_data/simplecalc_profile.json +0 -4
  195. package/test_data/sudomcp_import_profile.json +0 -4
  196. package/test_data/test_script_profile.json +0 -8
  197. package/tsconfig.json +0 -13
  198. package/vitest.config.ts +0 -39
  199. /package/dist/agent/src/{chat/utils → agent/tools/contentExtractors}/htmlToText.js +0 -0
@@ -1,4 +0,0 @@
1
- export interface ISessionMessageSender<Msg> {
2
- broadcast(msg: Msg): void;
3
- sendTo(userUUID: string, msg: Msg): void;
4
- }
@@ -1,175 +0,0 @@
1
- #!/usr/bin/env node
2
- // -*- typescript -*-
3
-
4
- import * as dotenv from "dotenv";
5
- import { getLogger } from "@xalia/xmcp/sdk";
6
- import { WebSocketServer } from "ws";
7
- import * as ws from "ws";
8
- import { IncomingMessage } from "http";
9
- import { ConnectionManager, IUserConnectionManager } from "./connectionManager";
10
- import { Database, resolveCompoundName, UserData } from "../data/database";
11
- import { SessionData } from "../data/dataModels";
12
- import { ChatFatalError } from "../protocol/errors";
13
- import { ServerToClient } from "../protocol/messages";
14
- import { SessionRegistry } from "./sessionRegistry";
15
-
16
- const DEVELOPMENT: boolean = process.env.DEVELOPMENT === "1";
17
-
18
- dotenv.config();
19
- if (DEVELOPMENT) {
20
- dotenv.config({ path: ".env.development" });
21
- }
22
-
23
- const logger = getLogger();
24
-
25
- /**
26
- * Extract error message from unknown error type
27
- */
28
- function extractErrorMessage(e: unknown): string {
29
- if (typeof e === "string") return e;
30
- if (e instanceof Error) return e.message;
31
- return "Unknown connection error";
32
- }
33
-
34
- /**
35
- * Send error response and close WebSocket connection
36
- */
37
- function sendErrorAndClose(ws: ws.WebSocket, message: string): void {
38
- try {
39
- sendErrorResponse(ws, message);
40
- } catch (closeError) {
41
- logger.error(`[server] Error closing connection:`, closeError);
42
- } finally {
43
- ws.close(4000, message);
44
- }
45
- }
46
-
47
- /**
48
- * Send error response but keep connection open
49
- */
50
- function sendErrorResponse(ws: ws.WebSocket, message: string): void {
51
- try {
52
- ws.send(
53
- JSON.stringify({
54
- t: "error",
55
- e: message,
56
- })
57
- );
58
- } catch (sendError) {
59
- logger.error(`[server] Error sending error response:`, sendError);
60
- }
61
- }
62
-
63
- export async function runServer(
64
- port: number,
65
- supabaseUrl: string,
66
- supabaseKey: string,
67
- xmcpUrl: string
68
- ): Promise<ws.Server> {
69
- return new Promise((r, _e) => {
70
- const wss = new WebSocketServer({ port });
71
- const db = new Database(supabaseUrl, supabaseKey);
72
- const createSessionRegistry = (
73
- connManager: IUserConnectionManager<ServerToClient>
74
- ) => {
75
- return new SessionRegistry(db, connManager, xmcpUrl);
76
- };
77
- const connectionManager = ConnectionManager.init(createSessionRegistry);
78
-
79
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
80
- wss.on("connection", async (ws: ws.WebSocket, req: IncomingMessage) => {
81
- try {
82
- logger.info(`[server] connection: ${req.url || "unknown"}`);
83
- logger.info(`[server] headers: ${JSON.stringify(req.headers)}`);
84
-
85
- // Extract API key from WebSocket subprotocol header (formatted as
86
- // comma-separated strings)
87
- const subprotocols = req.headers["sec-websocket-protocol"];
88
- if (!subprotocols) {
89
- throw new ChatFatalError("empty api key");
90
- }
91
-
92
- if (typeof subprotocols !== "string") {
93
- throw new ChatFatalError("subprotocols was not a string");
94
- }
95
- const protocols = subprotocols.split(",").map((p) => p.trim());
96
- const apiKey = protocols[0];
97
- if (!apiKey) {
98
- throw new ChatFatalError("empty api key");
99
- }
100
-
101
- // Handle via ConnectionManager (multi-session protocol only)
102
- await connectionManager.handleConnection(ws, apiKey, req);
103
- } catch (e) {
104
- logger.error(`[server] Connection error:`, e);
105
-
106
- // Extract error message consistently
107
- const errorMessage = extractErrorMessage(e);
108
-
109
- if (e instanceof ChatFatalError) {
110
- // Fatal errors: send error response and close connection
111
- sendErrorAndClose(ws, errorMessage);
112
- } else {
113
- // Non-fatal errors: send error response but keep connection open
114
- sendErrorResponse(ws, errorMessage);
115
- logger.error(`[server] Client error:`, errorMessage);
116
- }
117
- }
118
- });
119
-
120
- logger.info(`[server] started: ws://localhost:${String(port)}/`);
121
-
122
- r(wss);
123
- });
124
- }
125
-
126
- export async function resolveSessionIdFromIdentifier(
127
- db: Database,
128
- sessionIdentifier: string
129
- ): Promise<string | undefined> {
130
- logger.info(`[resolveSessionIdFromIdentifier] ${sessionIdentifier}`);
131
- let session: SessionData | undefined = undefined;
132
- const compound = resolveCompoundName(sessionIdentifier);
133
- logger.info(
134
- `[resolveSessionIdFromIdentifier] compound: ${JSON.stringify(compound)}`
135
- );
136
- if (typeof compound === "string") {
137
- // Interpret as an id
138
- session = await db.sessionGetDescriptorById(compound);
139
- } else {
140
- session = await db.sessionGetDescriptorByName(compound[0], compound[1]);
141
- }
142
- logger.info(`[resolveSessionIdFromIdentifier] ${JSON.stringify(session)}`);
143
- return session?.session_uuid;
144
- }
145
-
146
- /**
147
- * Try as id, then by user/name and return the AgentProfile uuid.
148
- */
149
- export async function resolveAgentProfileId(
150
- db: Database,
151
- userData: UserData,
152
- agentProfileIdentifier: string
153
- ): Promise<string | undefined> {
154
- let ap = await db.getSavedAgentProfileById(agentProfileIdentifier);
155
- logger.debug(`[resolveAgentProfileId]: by id: {JSON.stringify(ap)}`);
156
- if (ap) {
157
- return agentProfileIdentifier;
158
- }
159
-
160
- ap = await db.getSavedAgentProfileByName(
161
- userData.uuid,
162
- agentProfileIdentifier
163
- );
164
- logger.debug(`[resolveAgentProfileId]: by name: {JSON.stringify(ap)}`);
165
- if (ap) {
166
- return ap.uuid;
167
- }
168
-
169
- logger.debug("[resolveAgentProfileId]: agent profile not found");
170
- return undefined;
171
- }
172
-
173
- // Export main classes for external use
174
- export { ConnectionManager } from "./connectionManager";
175
- export { OpenSession } from "./openSession";
@@ -1,422 +0,0 @@
1
- import { v4 as uuidv4 } from "uuid";
2
- import { strict as assert } from "assert";
3
-
4
- import {
5
- AgentEx,
6
- IAgentToolProvider,
7
- ToolCallResult,
8
- ToolHandler,
9
- } from "../../agent/agent";
10
- import { makeParseArgsFn } from "./tools";
11
- import { Database } from "../data/database";
12
- import { DbSessionFiles } from "../data/dbSessionFiles";
13
- import {
14
- FileMetaData,
15
- SessionFileDescriptor,
16
- SessionFileEntry,
17
- getSessionFileMimeTypeFromDataUrl,
18
- } from "../data/dbSessionFileModels";
19
- import { ToolDescriptor } from "../../agent/llm";
20
-
21
- /**
22
- * Tool call result with specific form of metadata
23
- */
24
- export type ToolCallResultWithFileRef = ToolCallResult<FileMetaData>;
25
-
26
- /**
27
- * Calling code implements this to be informed of file changes.
28
- */
29
- export interface ISessionFileManagerEventHandler {
30
- onFileChanged(entry: SessionFileEntry, new_file: boolean): void;
31
- onFileDeleted(name: string): void;
32
- }
33
-
34
- /**
35
- * Interface to a set of files in a session.
36
- */
37
- export interface ISessionFileManager {
38
- /**
39
- * List file names, types and summaries,
40
- */
41
- listFiles(): SessionFileDescriptor[];
42
-
43
- /**
44
- * Retrieve file contents.
45
- */
46
- getFileContent(name: string): Promise<string>;
47
-
48
- /**
49
- * Delete a file
50
- */
51
- deleteFile(name: string): Promise<void>;
52
-
53
- /**
54
- * Create or update a file with the given name. Returns the name (which is
55
- * created if one is not passed in).
56
- */
57
- putFileContent(
58
- name: string | undefined,
59
- summary: string | undefined,
60
- data_url: string
61
- ): Promise<SessionFileDescriptor>;
62
-
63
- addEventHandler(eventHandler: ISessionFileManagerEventHandler): void;
64
- }
65
-
66
- /**
67
- * In-memory implementation of ISessionFileManager
68
- */
69
- export class MemoryFileManager implements ISessionFileManager {
70
- private readonly files: Map<string, SessionFileEntry>;
71
- private eventHandlers: ISessionFileManagerEventHandler[];
72
-
73
- constructor() {
74
- this.files = new Map();
75
- this.eventHandlers = [];
76
- }
77
-
78
- // ISessionFileManager.listFiles
79
- listFiles(): SessionFileDescriptor[] {
80
- return Array.from(this.files.values());
81
- }
82
-
83
- // ISessionFileManager.getFileContent
84
- getFileContent(name: string): Promise<string> {
85
- return new Promise((r, e) => {
86
- const entry = this.files.get(name);
87
- if (entry) {
88
- r(entry.data_url);
89
- } else {
90
- e(new Error(`no such file ${name}`));
91
- }
92
- });
93
- }
94
-
95
- // ISessionFileManager.deleteFile
96
- deleteFile(name: string): Promise<void> {
97
- return new Promise((r, e) => {
98
- if (this.files.has(name)) {
99
- this.files.delete(name);
100
- this.eventHandlers.forEach((eh) => {
101
- eh.onFileDeleted(name);
102
- });
103
- r();
104
- } else {
105
- e(new Error(`no such file ${name}`));
106
- }
107
- });
108
- }
109
-
110
- // ISessionFileManager.addFile
111
- putFileContent(
112
- name: string | undefined,
113
- summary: string | undefined,
114
- data_url: string
115
- ): Promise<SessionFileDescriptor> {
116
- return new Promise((r, e) => {
117
- if (!name) {
118
- name = uuidv4();
119
- }
120
- if (!summary) {
121
- summary = "";
122
- }
123
- if (!isSingleLine(summary)) {
124
- e(new Error("summary must no contain new-lines"));
125
- } else {
126
- const mime_type = getSessionFileMimeTypeFromDataUrl(data_url);
127
- const is_new = !this.files.has(name);
128
- const entry: SessionFileEntry = { name, mime_type, summary, data_url };
129
- this.eventHandlers.forEach((eh) => {
130
- eh.onFileChanged(entry, is_new);
131
- });
132
- this.files.set(name, entry);
133
- r({ name, mime_type, summary });
134
- }
135
- });
136
- }
137
-
138
- // ISessionFileManager.setEventHandler
139
- addEventHandler(eventHandler: ISessionFileManagerEventHandler) {
140
- this.eventHandlers.push(eventHandler);
141
- }
142
- }
143
-
144
- /**
145
- * Implementation of ISessionFileManager which can store files in the DB.
146
- */
147
- export class ChatSessionFileManager implements ISessionFileManager {
148
- private readonly sessionUUID: string;
149
- private readonly sfc: DbSessionFiles;
150
- private readonly fileMap: Map<string, SessionFileDescriptor>;
151
- private readonly fileDataCache: Map<string, string>;
152
- private eventHandlers: ISessionFileManagerEventHandler[];
153
-
154
- private constructor(
155
- sfc: DbSessionFiles,
156
- sessionUUID: string,
157
- fileMap: Map<string, SessionFileDescriptor>
158
- ) {
159
- this.sessionUUID = sessionUUID;
160
- this.sfc = sfc;
161
- this.eventHandlers = [];
162
- this.fileMap = fileMap;
163
- this.fileDataCache = new Map();
164
- }
165
-
166
- static async init(
167
- db: Database,
168
- sessionUUID: string
169
- ): Promise<ChatSessionFileManager> {
170
- const sfc = db.createTypedClient(DbSessionFiles);
171
- const fileList = await sfc.getFilesForSession(sessionUUID);
172
- const fileMap = new Map(fileList.map((f) => [f.name, f]));
173
- return new ChatSessionFileManager(sfc, sessionUUID, fileMap);
174
- }
175
-
176
- listFiles(): SessionFileDescriptor[] {
177
- return Array.from(this.fileMap.values());
178
- }
179
-
180
- async getFileContent(name: string): Promise<string> {
181
- const cached = this.fileDataCache.get(name);
182
- if (cached) {
183
- return cached;
184
- }
185
-
186
- if (!this.fileMap.has(name)) {
187
- throw new Error(`no file '${name}' in session '${this.sessionUUID}'`);
188
- }
189
-
190
- const fileData = await this.sfc.getFileContent(this.sessionUUID, name);
191
- if (!fileData) {
192
- // logically should not happen
193
- throw new Error(`empty file '${name}' in session '${this.sessionUUID}'`);
194
- }
195
-
196
- this.fileDataCache.set(name, fileData);
197
- return fileData;
198
- }
199
-
200
- // ISessionFileManager.deleteFile
201
- async deleteFile(name: string): Promise<void> {
202
- if (this.fileMap.has(name)) {
203
- this.fileMap.delete(name);
204
- this.fileDataCache.delete(name);
205
- await this.sfc.deleteFile(this.sessionUUID, name);
206
- this.eventHandlers.forEach((eh) => {
207
- eh.onFileDeleted(name);
208
- });
209
- } else {
210
- throw new Error(`no file '${name}' in session '${this.sessionUUID}'`);
211
- }
212
- }
213
-
214
- // ISessionFileManager.addFile
215
- async putFileContent(
216
- name: string | undefined,
217
- summary: string | undefined,
218
- data_url: string
219
- ): Promise<SessionFileDescriptor> {
220
- if (!name) {
221
- // TODO: have the LLM fill in the name?
222
- name = uuidv4();
223
- }
224
- // TODO: have the LLM create summary if missing?
225
-
226
- // If we have an existing entry in the cache then update it, otherwise
227
- // create a new one.
228
-
229
- const mime_type = getSessionFileMimeTypeFromDataUrl(data_url);
230
- const existingDesc = this.fileMap.get(name);
231
- if (existingDesc) {
232
- existingDesc.summary = summary;
233
- existingDesc.mime_type = mime_type;
234
- } else {
235
- this.fileMap.set(name, { name, summary, mime_type });
236
- }
237
-
238
- const is_new = !this.fileMap.has(name);
239
- this.fileDataCache.set(name, data_url);
240
- await this.sfc.setFileContent(this.sessionUUID, name, summary, data_url);
241
- this.eventHandlers.forEach((eh) => {
242
- assert(typeof name === "string");
243
- eh.onFileChanged({ name, summary, mime_type, data_url }, is_new);
244
- });
245
- return { name, mime_type, summary };
246
- }
247
-
248
- // ISessionFileManager.setEventHandler
249
- addEventHandler(eventHandler: ISessionFileManagerEventHandler) {
250
- this.eventHandlers.push(eventHandler);
251
- }
252
-
253
- clearAllFiles(): Promise<void> {
254
- this.fileMap.clear();
255
- this.fileDataCache.clear();
256
- return this.sfc.clearFiles(this.sessionUUID);
257
- }
258
-
259
- getSessionFileRelativeUrl(name: string): string {
260
- return `file+session:/./${name}`;
261
- }
262
-
263
- getSessionFileAbsoluteUrl(name: string): string {
264
- return `file+session://${this.sessionUUID}/${name}`;
265
- }
266
- }
267
-
268
- /**
269
- * Return the file list in a form easily parsable by the LLM.
270
- */
271
- export function createSessionFilesManagerPrompt(
272
- fm: ISessionFileManager
273
- ): string {
274
- const files = fm.listFiles();
275
- if (files.length === 0) {
276
- return "";
277
- }
278
- let prompt =
279
- "Files can be read/written as required. Create new files conservatively, " +
280
- "usually when complex content is requested. Use base64 for binary (image," +
281
- "pdf), ascii for text formats (html,markdown). " +
282
- "Available files:\nname,type,summary\n";
283
- for (const f of files) {
284
- prompt += `${f.name},${f.mime_type},${f.summary || ""}\n`;
285
- }
286
- return prompt;
287
- }
288
-
289
- const GET_FILE_CONTENT_TOOL: ToolDescriptor = {
290
- type: "function",
291
- function: {
292
- name: "get_file_content",
293
- description: "Obtain contents of file listed in system prompt, as data-url",
294
- parameters: {
295
- type: "object",
296
- properties: {
297
- name: {
298
- type: "string",
299
- description: "File name",
300
- },
301
- },
302
- required: ["name"],
303
- },
304
- },
305
- };
306
-
307
- const PUT_FILE_CONTENT_TOOL: ToolDescriptor = {
308
- type: "function",
309
- function: {
310
- name: "put_file_content",
311
- description: "Create or update file content",
312
- parameters: {
313
- type: "object",
314
- properties: {
315
- name: {
316
- type: "string",
317
- description: "File name",
318
- },
319
- summary: {
320
- type: "string",
321
- description: "Content summary",
322
- },
323
- data_url: {
324
- type: "string",
325
- description:
326
- "Content data-url: `data:<mime-type>[;<format>],<encoding>`",
327
- },
328
- },
329
- required: ["name", "summary", "data_url"],
330
- },
331
- },
332
- };
333
-
334
- const DELETE_FILE_CONTENT_TOOL: ToolDescriptor = {
335
- type: "function",
336
- function: {
337
- name: "delete_file_content",
338
- description: "Delete file",
339
- parameters: {
340
- type: "object",
341
- properties: {
342
- name: {
343
- type: "string",
344
- description: "File name",
345
- },
346
- },
347
- required: ["name"],
348
- },
349
- },
350
- };
351
-
352
- export function fileManagerTool(
353
- fileManager: ChatSessionFileManager
354
- ): IAgentToolProvider {
355
- // `get_file_content` tool
356
- //
357
- // LLM can read data from the file. To keep the context small, we overwrite
358
- // the data with the session file url after the LLM has seen it.
359
-
360
- const parseName = makeParseArgsFn(["name"] as const);
361
- const getFileContentFn: ToolHandler = async (
362
- _agent: AgentEx,
363
- args: unknown
364
- ): Promise<ToolCallResult> => {
365
- const { name } = parseName(args);
366
- const response = await fileManager.getFileContent(name);
367
- const overwriteResponse = fileManager.getSessionFileRelativeUrl(name);
368
- return { response, overwriteResponse };
369
- };
370
-
371
- // `set_file_content` tool
372
- //
373
- // Allows LLM to write data (as a data-url) to the session file manager. The
374
- // data is replaced by a session file url after being saved.
375
-
376
- const putArgs = ["name", "summary", "data_url"] as const;
377
- const parseNameSummaryDataUrl = makeParseArgsFn(putArgs);
378
- const putFileContentFn: ToolHandler = async (
379
- _: AgentEx,
380
- args: unknown
381
- ): Promise<ToolCallResultWithFileRef> => {
382
- const parsed = parseNameSummaryDataUrl(args);
383
- const { name, summary, data_url } = parsed;
384
- const desc = await fileManager.putFileContent(name, summary, data_url);
385
- const mimeType = getSessionFileMimeTypeFromDataUrl(data_url);
386
- parsed.data_url = fileManager.getSessionFileRelativeUrl(name);
387
- return {
388
- response: desc.name,
389
- overwriteArgs: JSON.stringify(parsed),
390
- _meta: {
391
- "xalia/fileUri": parsed.data_url,
392
- "xalia/fileMimeType": mimeType,
393
- },
394
- };
395
- };
396
-
397
- // `delete_file_content` tool
398
- //
399
- // Allows LLM to write data (as a data-url) to the session file manager. The
400
- // data is replaced by a session file url after being saved.
401
-
402
- const deleteFileFn: ToolHandler = async (_: AgentEx, args: unknown) => {
403
- const { name } = parseName(args);
404
- await fileManager.deleteFile(name);
405
- return { response: "" };
406
- };
407
-
408
- return {
409
- setup: (agent: AgentEx) => {
410
- agent.addAgentTool(GET_FILE_CONTENT_TOOL, getFileContentFn);
411
- agent.addAgentTool(PUT_FILE_CONTENT_TOOL, putFileContentFn);
412
- agent.addAgentTool(DELETE_FILE_CONTENT_TOOL, deleteFileFn);
413
- return new Promise<void>((r) => {
414
- r();
415
- });
416
- },
417
- };
418
- }
419
-
420
- function isSingleLine(str: string): boolean {
421
- return !/[\r\n]/.test(str);
422
- }