Bläddra i källkod

feat(tui): back chat replies with the active LLM

Ma 7 månader sedan
förälder
incheckning
55d375682a

+ 14 - 2
packages/core/src/__tests__/interaction-runtime.test.ts

@@ -193,6 +193,12 @@ describe("interaction runtime", () => {
   });
 
   it("answers chat-style requests without forcing a status summary", async () => {
+    const chat = vi.fn(async () => ({
+      __interaction: {
+        responseText: "你好,我在。当前没有活动书,要不要先说说你想写什么?",
+      },
+    }));
+
     const result = await runInteractionRequest({
       session: InteractionSessionSchema.parse({
         sessionId: "session-chat",
@@ -207,10 +213,16 @@ describe("interaction runtime", () => {
         bookId: "harbor",
         instruction: "hi",
       },
-      tools: makeTools(),
+      tools: makeTools({
+        chat,
+      }),
     });
 
-    expect(result.responseText).toContain("harbor");
+    expect(chat).toHaveBeenCalledWith("hi", {
+      bookId: "harbor",
+      automationMode: "semi",
+    });
+    expect(result.responseText).toContain("你好");
     expect(result.responseText).not.toContain("Current status");
     expect(result.session.events.map((event) => event.kind)).toEqual([
       "task.started",

+ 38 - 0
packages/core/src/interaction/project-tools.ts

@@ -6,7 +6,9 @@ import type {
   PipelineRunner,
   StateManager,
   ReviseMode,
+  LLMClient,
 } from "../index.js";
+import { chatCompletion } from "../index.js";
 import { executeEditTransaction } from "./edit-controller.js";
 import type { InteractionRuntimeTools } from "./runtime.js";
 
@@ -15,6 +17,8 @@ type StateLike = Pick<StateManager, "ensureControlDocuments" | "bookDir" | "load
 type InstrumentablePipelineLike = PipelineLike & {
   readonly config?: {
     logger?: Logger;
+    client?: LLMClient;
+    model?: string;
   };
 };
 
@@ -197,6 +201,40 @@ export function createInteractionToolsFromDeps(
 
   return {
     listBooks: () => state.listBooks(),
+    chat: async (input, options) => {
+      const bookLabel = options.bookId ?? "none";
+      const response = instrumentedPipeline.config?.client && instrumentedPipeline.config?.model
+        ? await chatCompletion(
+          instrumentedPipeline.config.client,
+          instrumentedPipeline.config.model,
+          [
+            {
+              role: "system",
+              content: [
+                "You are InkOS inside the terminal workbench.",
+                "Respond conversationally and briefly.",
+                "If there is no active book, help the user decide what to write next.",
+                "If there is an active book, keep the answer grounded in that book context.",
+              ].join(" "),
+            },
+            {
+              role: "user",
+              content: `activeBook=${bookLabel}\nautomationMode=${options.automationMode}\nmessage=${input}`,
+            },
+          ],
+          { temperature: 0.4, maxTokens: 240 },
+        )
+        : undefined;
+
+      return {
+        __interaction: {
+          responseText: response?.content?.trim()
+            || (options.bookId
+              ? `I’m here. Active book is ${options.bookId}.`
+              : "I’m here. No active book yet."),
+        },
+      };
+    },
     writeNextChapter: (bookId) => withPipelineInteractionTelemetry(
       instrumentedPipeline,
       bookId,

+ 23 - 7
packages/core/src/interaction/runtime.ts

@@ -14,6 +14,13 @@ type ReviseMode = "local-fix" | "rewrite";
 
 export interface InteractionRuntimeTools {
   readonly listBooks: () => Promise<ReadonlyArray<string>>;
+  readonly chat?: (
+    input: string,
+    options: {
+      readonly bookId?: string;
+      readonly automationMode: AutomationMode;
+    },
+  ) => Promise<unknown>;
   readonly writeNextChapter: (bookId: string) => Promise<unknown>;
   readonly reviseDraft: (bookId: string, chapterNumber: number, mode: ReviseMode) => Promise<unknown>;
   readonly patchChapterText: (
@@ -545,13 +552,22 @@ export async function runInteractionRequest(params: {
     case "chat": {
       const bookId = request.bookId ?? session.activeBookId;
       const prompt = request.instruction?.trim().toLowerCase() ?? "";
-      const responseText = /^(hi|hello|hey|你好|嗨|哈喽)$/i.test(prompt)
-        ? (bookId
-            ? `Hi. Active book is ${bookId}. Ask me to continue, revise a chapter, or explain what is blocked.`
-            : "Hi. No active book yet. Open a book, list books, or tell me what you want to write.")
-        : (bookId
-            ? `I’m here. Active book is ${bookId}. You can ask me to continue, revise a chapter, rewrite, change focus, or inspect why the pipeline stopped.`
-            : "I’m here. No active book is bound yet. Open a book, list books, or describe what you want to write.");
+      const toolResult = params.tools.chat
+        ? await params.tools.chat(request.instruction ?? "", {
+            bookId,
+            automationMode: session.automationMode,
+          })
+        : undefined;
+      const metadata = extractToolMetadata(toolResult);
+      const responseText = metadata.responseText ?? (
+        /^(hi|hello|hey|你好|嗨|哈喽)$/i.test(prompt)
+          ? (bookId
+              ? `Hi. Active book is ${bookId}. Ask me to continue, revise a chapter, or explain what is blocked.`
+              : "Hi. No active book yet. Open a book, list books, or tell me what you want to write.")
+          : (bookId
+              ? `I’m here. Active book is ${bookId}. You can ask me to continue, revise a chapter, rewrite, change focus, or inspect why the pipeline stopped.`
+              : "I’m here. No active book is bound yet. Open a book, list books, or describe what you want to write.")
+      );
       const completed = markCompleted(session);
       return {
         session: addEvent(completed, "task.completed", "completed", responseText),