chattercatcher 0.1.16 → 0.1.17

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/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import fs13 from "fs/promises";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "chattercatcher",
11
- version: "0.1.16",
11
+ version: "0.1.17",
12
12
  description: "\u672C\u5730\u4F18\u5148\u7684\u98DE\u4E66/Lark \u5BB6\u5EAD\u7FA4\u77E5\u8BC6\u5E93\u673A\u5668\u4EBA",
13
13
  type: "module",
14
14
  main: "dist/index.js",
@@ -894,6 +894,40 @@ function getGatewayStatus(config, secrets) {
894
894
  function normalizeBaseUrl(baseUrl) {
895
895
  return baseUrl.replace(/\/+$/, "");
896
896
  }
897
+ function toOpenAIMessage(message) {
898
+ return {
899
+ role: message.role,
900
+ content: message.content,
901
+ ...message.toolCallId ? { tool_call_id: message.toolCallId } : {},
902
+ ...message.toolCalls ? {
903
+ tool_calls: message.toolCalls.map((toolCall) => ({
904
+ id: toolCall.id,
905
+ type: "function",
906
+ function: {
907
+ name: toolCall.name,
908
+ arguments: JSON.stringify(toolCall.input)
909
+ }
910
+ }))
911
+ } : {}
912
+ };
913
+ }
914
+ function toOpenAITool(tool) {
915
+ return {
916
+ type: "function",
917
+ function: {
918
+ name: tool.name,
919
+ description: tool.description,
920
+ parameters: tool.inputSchema
921
+ }
922
+ };
923
+ }
924
+ function parseToolCalls(message) {
925
+ return message?.tool_calls?.map((toolCall) => ({
926
+ id: toolCall.id,
927
+ name: toolCall.function.name,
928
+ input: JSON.parse(toolCall.function.arguments)
929
+ })) ?? [];
930
+ }
897
931
  var OpenAICompatibleChatModel = class {
898
932
  constructor(options) {
899
933
  this.options = options;
@@ -911,7 +945,7 @@ var OpenAICompatibleChatModel = class {
911
945
  },
912
946
  body: JSON.stringify({
913
947
  model: this.options.model,
914
- messages,
948
+ messages: messages.map(toOpenAIMessage),
915
949
  temperature: this.options.temperature ?? 0.2
916
950
  })
917
951
  });
@@ -926,6 +960,35 @@ var OpenAICompatibleChatModel = class {
926
960
  }
927
961
  return content;
928
962
  }
963
+ async completeWithTools(messages, tools) {
964
+ if (!this.options.baseUrl || !this.options.apiKey || !this.options.model) {
965
+ throw new Error("LLM \u914D\u7F6E\u4E0D\u5B8C\u6574\u3002\u8BF7\u8FD0\u884C chattercatcher setup \u6216 chattercatcher settings\u3002");
966
+ }
967
+ const response = await fetch(`${normalizeBaseUrl(this.options.baseUrl)}/chat/completions`, {
968
+ method: "POST",
969
+ headers: {
970
+ authorization: `Bearer ${this.options.apiKey}`,
971
+ "content-type": "application/json"
972
+ },
973
+ body: JSON.stringify({
974
+ model: this.options.model,
975
+ messages: messages.map(toOpenAIMessage),
976
+ tools: tools.map(toOpenAITool),
977
+ tool_choice: "auto",
978
+ temperature: this.options.temperature ?? 0.2
979
+ })
980
+ });
981
+ if (!response.ok) {
982
+ const body = await response.text();
983
+ throw new Error(`LLM \u8BF7\u6C42\u5931\u8D25\uFF1A${response.status} ${body}`);
984
+ }
985
+ const data2 = await response.json();
986
+ const message = data2.choices?.[0]?.message;
987
+ return {
988
+ content: message?.content ?? "",
989
+ toolCalls: parseToolCalls(message)
990
+ };
991
+ }
929
992
  };
930
993
  var OpenAICompatibleEmbeddingModel = class {
931
994
  constructor(options) {
@@ -1612,6 +1675,73 @@ var MessageFtsRetriever = class {
1612
1675
  }
1613
1676
  };
1614
1677
 
1678
+ // src/rag/search-tools.ts
1679
+ var searchInputSchema = {
1680
+ type: "object",
1681
+ properties: {
1682
+ query: { type: "string", description: "Search query written by the model." },
1683
+ limit: { type: "number", description: "Maximum number of evidence blocks to return." }
1684
+ },
1685
+ required: ["query"],
1686
+ additionalProperties: false
1687
+ };
1688
+ function parseSearchInput(input2) {
1689
+ const rawQuery = typeof input2 === "object" && input2 !== null && "query" in input2 ? input2.query : void 0;
1690
+ if (typeof rawQuery !== "string") {
1691
+ throw new Error("\u641C\u7D22 query \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002");
1692
+ }
1693
+ const query = rawQuery.trim();
1694
+ if (!query) {
1695
+ throw new Error("\u641C\u7D22 query \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002");
1696
+ }
1697
+ const rawLimit = typeof input2 === "object" && input2 !== null && "limit" in input2 ? input2.limit : void 0;
1698
+ const numericLimit = typeof rawLimit === "number" && Number.isFinite(rawLimit) ? rawLimit : 5;
1699
+ const limit = Math.min(12, Math.max(1, Math.floor(numericLimit)));
1700
+ return { query, limit };
1701
+ }
1702
+ async function runRetriever(retriever, input2) {
1703
+ const { query, limit } = parseSearchInput(input2);
1704
+ const results = await retriever.retrieve(query);
1705
+ return results.slice(0, limit);
1706
+ }
1707
+ function createSearchTool(name, description, retriever) {
1708
+ return {
1709
+ name,
1710
+ description,
1711
+ inputSchema: searchInputSchema,
1712
+ execute: (input2) => runRetriever(retriever, input2)
1713
+ };
1714
+ }
1715
+ function createRagSearchTools(input2) {
1716
+ const tools = [
1717
+ createSearchTool(
1718
+ "hybrid_search",
1719
+ "Search across all indexed RAG evidence using the default hybrid retrieval strategy.",
1720
+ input2.hybrid
1721
+ ),
1722
+ createSearchTool(
1723
+ "search_messages",
1724
+ "Search chat messages only when the answer likely depends on message-level evidence.",
1725
+ input2.messages
1726
+ ),
1727
+ createSearchTool(
1728
+ "search_episodes",
1729
+ "Search episode summaries only when the answer likely depends on longer-running context.",
1730
+ input2.episodes
1731
+ )
1732
+ ];
1733
+ if (input2.semantic) {
1734
+ tools.push(
1735
+ createSearchTool(
1736
+ "semantic_search",
1737
+ "Search semantic vector evidence only when broader conceptual recall is needed.",
1738
+ input2.semantic
1739
+ )
1740
+ );
1741
+ }
1742
+ return tools;
1743
+ }
1744
+
1615
1745
  // src/rag/embedding.ts
1616
1746
  function cosineSimilarity(left, right) {
1617
1747
  if (left.length === 0 || right.length === 0 || left.length !== right.length) {
@@ -1766,6 +1896,20 @@ async function createHybridRetriever(input2) {
1766
1896
  }
1767
1897
  };
1768
1898
  }
1899
+ async function createAgenticRagSearchTools(input2) {
1900
+ const episodes = new EpisodeFtsRetriever(new EpisodeRepository(input2.database));
1901
+ const messages = new MessageFtsRetriever(input2.messages, { excludeMessageIds: input2.excludeMessageIds });
1902
+ const semantic = hasEmbeddingConfig(input2.config, input2.secrets) ? new VectorRetriever(
1903
+ createEmbeddingModel(input2.config, input2.secrets),
1904
+ new SqliteVectorStore(input2.database, { model: input2.config.embedding.model })
1905
+ ) : void 0;
1906
+ const hybrid = new HybridRetriever(semantic ? [episodes, messages, semantic] : [episodes, messages]);
1907
+ return {
1908
+ tools: createRagSearchTools({ hybrid, messages, episodes, semantic }),
1909
+ close: () => {
1910
+ }
1911
+ };
1912
+ }
1769
1913
 
1770
1914
  // src/doctor/checks.ts
1771
1915
  function pass(name, message) {
@@ -2454,12 +2598,99 @@ async function generateGroundedAnswer(input2) {
2454
2598
  };
2455
2599
  }
2456
2600
 
2457
- // src/rag/qa-service.ts
2458
- async function askWithRag(input2) {
2459
- const evidence = await input2.retriever.retrieve(input2.question);
2601
+ // src/rag/agentic-qa-service.ts
2602
+ var DEFAULT_MAX_MODEL_TURNS = 4;
2603
+ var DEFAULT_MAX_TOOL_CALLS = 8;
2604
+ var DEFAULT_MAX_EVIDENCE = 12;
2605
+ var NO_EVIDENCE_ANSWER = "\u4E0D\u77E5\u9053\u3002\u5F53\u524D\u672C\u5730\u77E5\u8BC6\u5E93\u6CA1\u6709\u68C0\u7D22\u5230\u8DB3\u591F\u8BC1\u636E\u3002";
2606
+ var AGENTIC_SYSTEM_PROMPT = "\u4F60\u662F\u672C\u5730\u77E5\u8BC6\u4FE1\u606F\u6536\u96C6\u4EE3\u7406\u3002\u4F60\u7684\u804C\u8D23\u662F\u56F4\u7ED5\u7528\u6237\u95EE\u9898\u51B3\u5B9A\u662F\u5426\u8C03\u7528\u641C\u7D22\u5DE5\u5177\u3001\u9009\u62E9\u5408\u9002\u7684\u5DE5\u5177\u548C\u67E5\u8BE2\u8BCD\uFF0C\u5E76\u6839\u636E\u5F53\u524D\u7ED3\u679C\u51B3\u5B9A\u662F\u5426\u7EE7\u7EED\u641C\u7D22\u3002\u4E0D\u8981\u7F16\u9020\u4EFB\u4F55\u8BC1\u636E\u6216\u58F0\u79F0\u770B\u8FC7\u672A\u68C0\u7D22\u5230\u7684\u5185\u5BB9\u3002\u4F60\u7684\u8F93\u51FA\u53EA\u7528\u4E8E\u6536\u96C6\u8BC1\u636E\uFF0C\u6700\u7EC8\u7B54\u6848\u4F1A\u7531\u53E6\u4E00\u4E2A\u57FA\u4E8E\u8BC1\u636E\u7684\u6B65\u9AA4\u751F\u6210\u3002";
2607
+ function toToolResultContent(results) {
2608
+ return JSON.stringify(
2609
+ results.map((item) => ({
2610
+ id: item.id,
2611
+ text: item.text,
2612
+ score: item.score,
2613
+ source: item.source
2614
+ }))
2615
+ );
2616
+ }
2617
+ function toToolErrorContent(message) {
2618
+ return JSON.stringify({ error: message });
2619
+ }
2620
+ function dedupeEvidence(evidence, maxEvidence) {
2621
+ const deduped = [];
2622
+ const seen = /* @__PURE__ */ new Set();
2623
+ for (const item of evidence) {
2624
+ if (seen.has(item.id)) {
2625
+ continue;
2626
+ }
2627
+ seen.add(item.id);
2628
+ deduped.push(item);
2629
+ if (deduped.length >= maxEvidence) {
2630
+ break;
2631
+ }
2632
+ }
2633
+ return deduped;
2634
+ }
2635
+ async function askWithAgenticRag(input2) {
2636
+ if (!input2.model.completeWithTools) {
2637
+ throw new Error("\u5F53\u524D LLM \u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u5DE5\u5177\u8C03\u7528\u3002");
2638
+ }
2639
+ const maxModelTurns = input2.maxModelTurns ?? DEFAULT_MAX_MODEL_TURNS;
2640
+ const maxToolCalls = input2.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
2641
+ const maxEvidence = input2.maxEvidence ?? DEFAULT_MAX_EVIDENCE;
2642
+ const messages = [
2643
+ { role: "system", content: AGENTIC_SYSTEM_PROMPT },
2644
+ { role: "user", content: input2.question }
2645
+ ];
2646
+ const toolsByName = new Map(input2.tools.map((tool) => [tool.name, tool]));
2647
+ let evidence = [];
2648
+ let toolCallsUsed = 0;
2649
+ for (let turn = 0; turn < maxModelTurns; turn += 1) {
2650
+ const assistantResult = await input2.model.completeWithTools(messages, input2.tools);
2651
+ messages.push({
2652
+ role: "assistant",
2653
+ content: assistantResult.content,
2654
+ toolCalls: assistantResult.toolCalls
2655
+ });
2656
+ if (assistantResult.toolCalls.length === 0) {
2657
+ break;
2658
+ }
2659
+ for (const toolCall of assistantResult.toolCalls) {
2660
+ if (toolCallsUsed >= maxToolCalls) {
2661
+ break;
2662
+ }
2663
+ toolCallsUsed += 1;
2664
+ const tool = toolsByName.get(toolCall.name);
2665
+ if (!tool) {
2666
+ messages.push({
2667
+ role: "tool",
2668
+ toolCallId: toolCall.id,
2669
+ content: toToolErrorContent(`\u672A\u77E5\u5DE5\u5177\uFF1A${toolCall.name}`)
2670
+ });
2671
+ continue;
2672
+ }
2673
+ try {
2674
+ const results = await tool.execute(toolCall.input);
2675
+ evidence = dedupeEvidence([...evidence, ...results], maxEvidence);
2676
+ messages.push({
2677
+ role: "tool",
2678
+ toolCallId: toolCall.id,
2679
+ content: toToolResultContent(results)
2680
+ });
2681
+ } catch (error) {
2682
+ const message = error instanceof Error ? error.message : String(error);
2683
+ messages.push({
2684
+ role: "tool",
2685
+ toolCallId: toolCall.id,
2686
+ content: toToolErrorContent(message)
2687
+ });
2688
+ }
2689
+ }
2690
+ }
2460
2691
  if (evidence.length === 0) {
2461
2692
  return {
2462
- answer: "\u4E0D\u77E5\u9053\u3002\u5F53\u524D\u672C\u5730\u77E5\u8BC6\u5E93\u6CA1\u6709\u68C0\u7D22\u5230\u8DB3\u591F\u8BC1\u636E\u3002",
2693
+ answer: NO_EVIDENCE_ANSWER,
2463
2694
  citations: []
2464
2695
  };
2465
2696
  }
@@ -2577,7 +2808,7 @@ var FeishuQuestionHandler = class {
2577
2808
  }
2578
2809
  const questionMessageId = payload.event?.message?.message_id;
2579
2810
  await this.acknowledgeQuestion(decision.chatId, questionMessageId);
2580
- const { retriever, close } = await createHybridRetriever({
2811
+ const { tools, close } = await createAgenticRagSearchTools({
2581
2812
  config: this.options.config,
2582
2813
  secrets: this.options.secrets,
2583
2814
  database: this.options.database,
@@ -2586,9 +2817,9 @@ var FeishuQuestionHandler = class {
2586
2817
  });
2587
2818
  try {
2588
2819
  try {
2589
- const result = await askWithRag({
2820
+ const result = await askWithAgenticRag({
2590
2821
  question: decision.question,
2591
- retriever,
2822
+ tools,
2592
2823
  model: this.options.model
2593
2824
  });
2594
2825
  const citations = formatCitations(result.citations);
@@ -3433,6 +3664,22 @@ async function processMessagesNow(input2) {
3433
3664
  };
3434
3665
  }
3435
3666
 
3667
+ // src/rag/qa-service.ts
3668
+ async function askWithRag(input2) {
3669
+ const evidence = await input2.retriever.retrieve(input2.question);
3670
+ if (evidence.length === 0) {
3671
+ return {
3672
+ answer: "\u4E0D\u77E5\u9053\u3002\u5F53\u524D\u672C\u5730\u77E5\u8BC6\u5E93\u6CA1\u6709\u68C0\u7D22\u5230\u8DB3\u591F\u8BC1\u636E\u3002",
3673
+ citations: []
3674
+ };
3675
+ }
3676
+ return generateGroundedAnswer({
3677
+ question: input2.question,
3678
+ evidence,
3679
+ model: input2.model
3680
+ });
3681
+ }
3682
+
3436
3683
  // src/update/npm-updater.ts
3437
3684
  import { execFile } from "child_process";
3438
3685
  import { promisify } from "util";