amalfa 1.0.33 → 1.0.35

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalfa",
3
- "version": "1.0.33",
3
+ "version": "1.0.35",
4
4
  "description": "Local-first knowledge graph engine for AI agents. Transforms markdown into searchable memory with MCP protocol.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/pjsvis/amalfa#readme",
@@ -4,19 +4,39 @@ import { DaemonManager } from "../utils/DaemonManager";
4
4
 
5
5
  export async function chatLoop() {
6
6
  const manager = new DaemonManager();
7
- const status = await manager.checkSonarAgent();
7
+ let status = await manager.checkSonarAgent();
8
8
 
9
9
  if (!status.running) {
10
- console.log(
11
- "❌ Sonar Agent is not running. Start it with: amalfa sonar start",
12
- );
13
- process.exit(1);
10
+ console.log("🚀 Sonar Agent not running. Starting it now...");
11
+ await manager.startSonarAgent();
12
+ // Wait for it to be ready
13
+ await new Promise((resolve) => setTimeout(resolve, 2000));
14
+ status = await manager.checkSonarAgent();
15
+ if (!status.running) {
16
+ console.log(
17
+ "❌ Failed to start Sonar Agent. Check logs: .amalfa/logs/sonar.log",
18
+ );
19
+ process.exit(1);
20
+ }
21
+ console.log("✅ Sonar Agent started.\n");
14
22
  }
15
23
 
16
24
  const BASE_URL = `http://localhost:${status.port}`;
17
25
  let sessionId: string | undefined;
18
26
 
19
- console.log(`💬 AMALFA Corpus Assistant (${status.activeModel || "Sonar"})`);
27
+ // Fetch health to get provider info
28
+ let providerInfo = "";
29
+ try {
30
+ const health = (await fetch(`${BASE_URL}/health`).then((r) =>
31
+ r.json(),
32
+ )) as { provider?: string; model?: string };
33
+ const providerLabel = health.provider === "cloud" ? "☁️ Cloud" : "💻 Local";
34
+ providerInfo = ` [${providerLabel}: ${health.model || "unknown"}]`;
35
+ } catch {
36
+ providerInfo = "";
37
+ }
38
+
39
+ console.log(`💬 AMALFA Corpus Assistant${providerInfo}`);
20
40
  console.log(" Type 'exit' or 'quit' to leave.\n");
21
41
 
22
42
  const rl = createInterface({
@@ -149,8 +149,18 @@ function startServer(port: number) {
149
149
 
150
150
  // Health check
151
151
  if (url.pathname === "/health") {
152
+ const cfg = await loadConfig();
153
+ const provider = cfg.sonar.cloud?.enabled ? "cloud" : "local";
154
+ const model = cfg.sonar.cloud?.enabled
155
+ ? cfg.sonar.cloud.model
156
+ : inferenceState.ollamaModel || cfg.sonar.model;
152
157
  return Response.json(
153
- { status: "ok", ollama: inferenceState.ollamaAvailable },
158
+ {
159
+ status: "ok",
160
+ ollama: inferenceState.ollamaAvailable,
161
+ provider,
162
+ model,
163
+ },
154
164
  { headers: corsHeaders },
155
165
  );
156
166
  }
@@ -197,14 +197,22 @@ export async function handleSearchAnalysis(
197
197
  {
198
198
  role: "system",
199
199
  content:
200
- 'Analyze search queries and extract intent, entities and suggested filters. Return JSON: { "intent": "", "entities": [], "filters": {} }',
200
+ 'Analyze the user query. Extract the search intent, key entities, and any implicit filters. You MUST return valid JSON. Example: { "intent": "informational", "entities": ["vector"], "filters": {} }. Do not include any text outside the JSON object.',
201
201
  },
202
202
  { role: "user", content: query },
203
203
  ],
204
204
  { temperature: 0.1, format: "json" },
205
205
  );
206
206
 
207
- return JSON.parse(response.message.content);
207
+ const parsed = safeJsonParse(response.message.content);
208
+ if (!parsed) {
209
+ log.warn(
210
+ { content: response.message.content },
211
+ "Failed to parse JSON response, using fallback",
212
+ );
213
+ return { intent: "search", entities: [query], filters: {} };
214
+ }
215
+ return parsed;
208
216
  } catch (error) {
209
217
  log.error({ error, query }, "Query analysis failed");
210
218
  throw error;
@@ -250,7 +258,10 @@ export async function handleResultReranking(
250
258
 
251
259
  const content = response.message.content;
252
260
  try {
253
- const rankings = JSON.parse(content);
261
+ const rankings = safeJsonParse(content);
262
+ if (!rankings || !Array.isArray(rankings))
263
+ throw new Error("Invalid JSON");
264
+
254
265
  return results.map((result, idx) => {
255
266
  const ranking = rankings.find(
256
267
  (r: { index: number }) => r.index === idx + 1,
@@ -283,19 +294,25 @@ export async function handleContextExtraction(
283
294
  {
284
295
  role: "system",
285
296
  content:
286
- "Extract the most relevant 200-300 character snippet from the document for the given query.",
297
+ "You are a helpful assistant. Extract the exact text snippet from the document that answers the query. Return ONLY the snippet text. If the answer is not found, return the most relevant paragraph.",
287
298
  },
288
299
  {
289
300
  role: "user",
290
- content: `Query: ${query}\nDocument [${result.id}]:\n${result.content.slice(0, 4000)}`,
301
+ content: `Query: ${query}\n\nDocument Text:\n${result.content.slice(0, 4000)}`,
291
302
  },
292
303
  ],
293
- { temperature: 0 },
304
+ { temperature: 0.1 },
294
305
  );
295
306
 
307
+ const snippet = response.message.content.trim();
308
+
309
+ // Fallback if model refuses to extract or returns empty
310
+ const finalSnippet =
311
+ snippet.length > 5 ? snippet : result.content.slice(0, 300).trim();
312
+
296
313
  return {
297
314
  id: result.id,
298
- snippet: response.message.content.trim(),
315
+ snippet: finalSnippet,
299
316
  };
300
317
  } catch (error) {
301
318
  log.error({ error, docId: result.id }, "Context extraction failed");
@@ -520,17 +537,11 @@ Return JSON: { "action": "SEARCH"|"READ"|"EXPLORE"|"FINISH", "query": "...", "no
520
537
  nodeId?: string;
521
538
  reasoning: string;
522
539
  answer?: string;
523
- };
524
- try {
525
- decision = JSON.parse(content);
526
- } catch {
527
- // Try to extract JSON from markdown blocks
528
- const match = content.match(/\{[\s\S]*\}/);
529
- if (match) {
530
- decision = JSON.parse(match[0]);
531
- } else {
532
- throw new Error("Could not parse JSON from response");
533
- }
540
+ } | null = null;
541
+
542
+ decision = safeJsonParse(content);
543
+ if (!decision) {
544
+ throw new Error("Could not parse JSON from response");
534
545
  }
535
546
  output += `> **Reasoning:** ${decision.reasoning}\n\n`;
536
547
 
@@ -635,13 +646,7 @@ Return JSON: { "answered": true|false, "missing_info": "...", "final_answer": ".
635
646
  missing_info: string;
636
647
  final_answer: string;
637
648
  };
638
- let audit: AuditResult | null = null;
639
- try {
640
- audit = JSON.parse(resultSnippet);
641
- } catch {
642
- const match = resultSnippet.match(/\{[\s\S]*\}/);
643
- audit = match ? JSON.parse(match[0]) : null;
644
- }
649
+ const audit = safeJsonParse(resultSnippet) as AuditResult | null;
645
650
 
646
651
  if (audit) {
647
652
  if (!audit.answered) {
@@ -660,3 +665,23 @@ Return JSON: { "answered": true|false, "missing_info": "...", "final_answer": ".
660
665
 
661
666
  return output;
662
667
  }
668
+
669
+ /**
670
+ * Helper to safely parse JSON from LLM responses, handling markdown blocks
671
+ */
672
+ function safeJsonParse(content: string): any {
673
+ try {
674
+ return JSON.parse(content);
675
+ } catch {
676
+ // Try to extract JSON from markdown blocks
677
+ const match = content.match(/\{[\s\S]*\}/);
678
+ if (match) {
679
+ try {
680
+ return JSON.parse(match[0]);
681
+ } catch {
682
+ return null;
683
+ }
684
+ }
685
+ return null;
686
+ }
687
+ }