cite-agent 1.3.8__tar.gz → 1.3.9__tar.gz

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.

Potentially problematic release.


This version of cite-agent might be problematic. Click here for more details.

Files changed (81) hide show
  1. {cite_agent-1.3.8/cite_agent.egg-info → cite_agent-1.3.9}/PKG-INFO +1 -1
  2. cite_agent-1.3.9/cite_agent/__version__.py +1 -0
  3. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/cli.py +180 -7
  4. cite_agent-1.3.9/cite_agent/conversation_archive.py +152 -0
  5. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/enhanced_ai_agent.py +547 -192
  6. {cite_agent-1.3.8 → cite_agent-1.3.9/cite_agent.egg-info}/PKG-INFO +1 -1
  7. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent.egg-info/SOURCES.txt +11 -3
  8. {cite_agent-1.3.8 → cite_agent-1.3.9}/docs/BETA_LAUNCH_CHECKLIST.md +2 -0
  9. cite_agent-1.3.9/docs/BETA_PITCH_v1.3.9.md +95 -0
  10. cite_agent-1.3.9/docs/BETA_SHOWCASE_GUIDE.md +43 -0
  11. cite_agent-1.3.9/docs/DEV_NOTES_2025-10-30.md +8 -0
  12. {cite_agent-1.3.8 → cite_agent-1.3.9}/setup.py +1 -1
  13. cite_agent-1.3.9/tests/enhanced/conftest.py +11 -0
  14. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/enhanced/test_account_client.py +6 -5
  15. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/enhanced/test_archive_agent.py +30 -16
  16. cite_agent-1.3.9/tests/enhanced/test_autonomy_harness.py +124 -0
  17. cite_agent-1.3.9/tests/enhanced/test_conversation_archive.py +41 -0
  18. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/enhanced/test_enhanced_agent_runtime.py +15 -64
  19. cite_agent-1.3.9/tests/enhanced/test_financial_planner.py +59 -0
  20. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/enhanced/test_setup_config.py +1 -5
  21. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/integration_test.py +12 -12
  22. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/test_truth_seeking_comprehensive.py +1 -2
  23. cite_agent-1.3.8/cite_agent/__version__.py +0 -1
  24. {cite_agent-1.3.8 → cite_agent-1.3.9}/LICENSE +0 -0
  25. {cite_agent-1.3.8 → cite_agent-1.3.9}/MANIFEST.in +0 -0
  26. {cite_agent-1.3.8 → cite_agent-1.3.9}/README.md +0 -0
  27. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/__init__.py +0 -0
  28. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/__main__.py +0 -0
  29. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/account_client.py +0 -0
  30. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/agent_backend_only.py +0 -0
  31. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/ascii_plotting.py +0 -0
  32. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/auth.py +0 -0
  33. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/backend_only_client.py +0 -0
  34. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/cli_conversational.py +0 -0
  35. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/cli_enhanced.py +0 -0
  36. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/cli_workflow.py +0 -0
  37. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/dashboard.py +0 -0
  38. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/project_detector.py +0 -0
  39. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/rate_limiter.py +0 -0
  40. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/session_manager.py +0 -0
  41. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/setup_config.py +0 -0
  42. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/streaming_ui.py +0 -0
  43. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/telemetry.py +0 -0
  44. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/ui.py +0 -0
  45. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/updater.py +0 -0
  46. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/web_search.py +0 -0
  47. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/workflow.py +0 -0
  48. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent/workflow_integration.py +0 -0
  49. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent.egg-info/dependency_links.txt +0 -0
  50. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent.egg-info/entry_points.txt +0 -0
  51. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent.egg-info/requires.txt +0 -0
  52. {cite_agent-1.3.8 → cite_agent-1.3.9}/cite_agent.egg-info/top_level.txt +0 -0
  53. {cite_agent-1.3.8 → cite_agent-1.3.9}/docs/BETA_RELEASE_CHECKLIST.md +0 -0
  54. {cite_agent-1.3.8 → cite_agent-1.3.9}/docs/ENHANCED_CAPABILITIES.md +0 -0
  55. {cite_agent-1.3.8 → cite_agent-1.3.9}/docs/GROQ_RATE_LIMITS.md +0 -0
  56. {cite_agent-1.3.8 → cite_agent-1.3.9}/docs/INSTALL.md +0 -0
  57. {cite_agent-1.3.8 → cite_agent-1.3.9}/docs/PUBLISHING_PYPI.md +0 -0
  58. {cite_agent-1.3.8 → cite_agent-1.3.9}/docs/SECURE_PACKAGING_GUIDE.md +0 -0
  59. {cite_agent-1.3.8 → cite_agent-1.3.9}/docs/SECURITY_AUDIT.md +0 -0
  60. {cite_agent-1.3.8 → cite_agent-1.3.9}/docs/USER_GETTING_STARTED.md +0 -0
  61. {cite_agent-1.3.8 → cite_agent-1.3.9}/docs/playbooks/BETA_LAUNCH_PLAYBOOK.md +0 -0
  62. {cite_agent-1.3.8 → cite_agent-1.3.9}/requirements.txt +0 -0
  63. {cite_agent-1.3.8 → cite_agent-1.3.9}/setup.cfg +0 -0
  64. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/beta_launch_test_suite.py +0 -0
  65. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/enhanced/test_reasoning_engine.py +0 -0
  66. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/enhanced/test_tool_framework.py +0 -0
  67. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/session_affirmation.py +0 -0
  68. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/test_cli_direct.py +0 -0
  69. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/test_end_to_end.py +0 -0
  70. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/test_setup_flow.py +0 -0
  71. /cite_agent-1.3.8/tests/test_version_1.0.4.py → /cite_agent-1.3.9/tests/test_version_1_0_4.py +0 -0
  72. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/validation/test_accuracy_system.py +0 -0
  73. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/validation/test_agent_live.py +0 -0
  74. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/validation/test_backend_local.py +0 -0
  75. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/validation/test_cerebras_comparison.py +0 -0
  76. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/validation/test_improved_prompt.py +0 -0
  77. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/validation/test_qualitative_robustness.py +0 -0
  78. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/validation/test_qualitative_system.py +0 -0
  79. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/validation/test_truth_seeking_chinese.py +0 -0
  80. {cite_agent-1.3.8 → cite_agent-1.3.9}/tests/validation/test_truth_seeking_real.py +0 -0
  81. /cite_agent-1.3.8/tests/validation/test_truth_seeking_comprehensive.py → /cite_agent-1.3.9/tests/validation/test_truth_seeking_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cite-agent
3
- Version: 1.3.8
3
+ Version: 1.3.9
4
4
  Summary: Terminal AI assistant for academic research with citation verification
5
5
  Home-page: https://github.com/Spectating101/cite-agent
6
6
  Author: Cite-Agent Team
@@ -0,0 +1 @@
1
+ __version__ = "1.3.9"
@@ -6,13 +6,15 @@ Provides a terminal interface similar to cursor-agent
6
6
 
7
7
  import argparse
8
8
  import asyncio
9
+ import json
9
10
  import os
10
11
  import random
11
12
  import sys
12
13
  import time
14
+ import hashlib
13
15
  from datetime import datetime
14
16
  from pathlib import Path
15
- from typing import Optional
17
+ from typing import Optional, Dict
16
18
 
17
19
  from rich import box
18
20
  from rich.console import Console
@@ -28,6 +30,26 @@ from .cli_workflow import WorkflowCLI
28
30
  from .workflow import WorkflowManager, Paper, parse_paper_from_response
29
31
  from .session_manager import SessionManager
30
32
 
33
+ PRESET_SCENARIOS: Dict[str, Dict[str, str]] = {
34
+ "Research sprint": {
35
+ "prompt": "Run a literature review on retrieval-augmented generation, summarise three key papers and cite sources.",
36
+ "highlight": "Archive API + guardrails"
37
+ },
38
+ "Data audit": {
39
+ "prompt": "Inspect sales_data.csv, perform exploratory stats, and flag any anomalies worth investigating.",
40
+ "highlight": "Shell analytics + guardrails"
41
+ },
42
+ "Financial briefing": {
43
+ "prompt": "Compare NVDA and AMD revenue and margin trends for the last 4 quarters using FinSight.",
44
+ "highlight": "FinSight multi-ticker"
45
+ },
46
+ "Team handoff": {
47
+ "prompt": "Summarise our last session and note any follow-up tasks saved in the archive for project alpha.",
48
+ "highlight": "Archive memory"
49
+ },
50
+ }
51
+
52
+
31
53
  class NocturnalCLI:
32
54
  """Command Line Interface for Cite Agent"""
33
55
 
@@ -53,6 +75,18 @@ class NocturnalCLI:
53
75
  "Remember the sandbox: prefix shell commands with [bold]![/] to execute safe utilities only.",
54
76
  "If you see an auto-update notice, the CLI will restart itself to load the latest build.",
55
77
  ]
78
+ self._default_artifacts = Path("artifacts_autonomy.json")
79
+
80
+ def _record_session_event(self, success: bool) -> None:
81
+ try:
82
+ manager = TelemetryManager.get()
83
+ email = os.getenv("NOCTURNAL_ACCOUNT_EMAIL", "")
84
+ payload = {"success": bool(success)}
85
+ if email:
86
+ payload["user"] = hashlib.sha256(email.encode("utf-8")).hexdigest()[:16]
87
+ manager.record("session_login", payload)
88
+ except Exception:
89
+ pass
56
90
 
57
91
  def handle_user_friendly_session(self):
58
92
  """Handle session management with user-friendly interface"""
@@ -78,7 +112,9 @@ class NocturnalCLI:
78
112
  # Handle user-friendly session management (skip prompts in non-interactive mode)
79
113
  if not non_interactive:
80
114
  if not self.handle_user_friendly_session():
115
+ self._record_session_event(False)
81
116
  return False
117
+ self._record_session_event(True)
82
118
 
83
119
  self._show_intro_panel()
84
120
 
@@ -196,6 +232,92 @@ class NocturnalCLI:
196
232
  box=box.ROUNDED,
197
233
  )
198
234
  self.console.print(panel)
235
+
236
+ def show_presets(self) -> None:
237
+ table = Table(title="🚀 Beta Showcase Presets", box=box.ROUNDED, show_edge=True)
238
+ table.add_column("Scenario", style="bold cyan")
239
+ table.add_column("Prompt", style="white")
240
+ table.add_column("Highlights", style="magenta")
241
+ for name, payload in PRESET_SCENARIOS.items():
242
+ table.add_row(name, payload["prompt"], payload["highlight"])
243
+ self.console.print(table)
244
+ self.console.print("[dim]Tip: run [/dim][bold]nocturnal \"<prompt>\"[/bold][dim] to execute a preset immediately.[/dim]")
245
+
246
+ def show_metrics(self, artifacts: Optional[Path] = None) -> None:
247
+ artifacts_path = artifacts or self._default_artifacts
248
+ if not artifacts_path.exists():
249
+ self.console.print(
250
+ "[warning]No metrics file found.[/warning] Run [bold]python3 scripts/run_beta_showcase.py[/bold] first."
251
+ )
252
+ return
253
+
254
+ try:
255
+ payload = json.loads(artifacts_path.read_text())
256
+ except Exception as exc:
257
+ self.console.print(f"[error]Failed to parse {artifacts_path}: {exc}[/error]")
258
+ return
259
+
260
+ metrics = payload.get("_metrics")
261
+ if not metrics:
262
+ self.console.print(
263
+ "[warning]Metrics summary missing. Regenerate the file with [/warning]"
264
+ "[bold]python3 scripts/run_beta_showcase.py[/bold]."
265
+ )
266
+ return
267
+
268
+ table = Table(title="📊 Beta Harness Summary", box=box.ROUNDED)
269
+ table.add_column("Metric", style="bold green")
270
+ table.add_column("Value", style="white")
271
+ table.add_row("Scenarios", str(metrics.get("scenario_count", "-")))
272
+ elapsed = metrics.get("total_elapsed", 0.0)
273
+ table.add_row("Total elapsed", f"{elapsed:.2f}s")
274
+ guard = metrics.get("guardrail_pass_rate", 0.0)
275
+ table.add_row("Guardrail pass rate", f"{guard:.1%}")
276
+
277
+ tool_usage = metrics.get("tool_usage", {})
278
+ if tool_usage:
279
+ usage_lines = [f"{tool}: {count}" for tool, count in tool_usage.items()]
280
+ table.add_row("Tool invocations", "\n".join(usage_lines))
281
+
282
+ self.console.print(table)
283
+
284
+ guardrail_findings = []
285
+ for name, scenario in payload.items():
286
+ if not isinstance(scenario, dict) or name.startswith("_"):
287
+ continue
288
+ quality = scenario.get("quality_checks")
289
+ if not quality:
290
+ continue
291
+ if not all(quality.values()):
292
+ guardrail_findings.append((name, quality))
293
+
294
+ if guardrail_findings:
295
+ warn_table = Table(title="⚠️ Guardrails needing attention", box=box.ROUNDED, style="yellow")
296
+ warn_table.add_column("Scenario", style="bold")
297
+ warn_table.add_column("Checks", style="white")
298
+ for scenario, checks in guardrail_findings:
299
+ failed = [f"{key}={val}" for key, val in checks.items()]
300
+ warn_table.add_row(scenario, ", ".join(failed))
301
+ self.console.print(warn_table)
302
+ else:
303
+ self.console.print("[success]All guardrails passed.[/success]")
304
+
305
+ def show_token_report(self) -> None:
306
+ try:
307
+ from scripts.token_report import build_token_report
308
+ except Exception as exc: # pragma: no cover - import convenience
309
+ self.console.print(f"[error]Failed to import token report tool: {exc}[/error]")
310
+ return
311
+
312
+ root = Path(os.getenv("NOCTURNAL_HOME", str(Path.home() / ".nocturnal_archive")))
313
+ report = build_token_report(root)
314
+ table = Table(title="🪙 Token Usage", box=box.ROUNDED)
315
+ table.add_column("User (hashed)", style="cyan")
316
+ table.add_column("Tokens", style="white", justify="right")
317
+ for user, tokens in report["per_user"].items():
318
+ table.add_row(user, f"{tokens:.0f}")
319
+ self.console.print(table)
320
+ self.console.print(f"[dim]Total tokens: {report['total_tokens']:.0f}[/dim]")
199
321
 
200
322
  def _enforce_latest_build(self):
201
323
  """Ensure the CLI is running the most recent published build."""
@@ -309,25 +431,43 @@ class NocturnalCLI:
309
431
  try:
310
432
  from rich.spinner import Spinner
311
433
  from rich.live import Live
312
-
313
- # Show loading indicator while processing
314
- with Live(Spinner("dots", text="[dim]Thinking...[/dim]"), console=self.console, transient=True):
434
+
435
+ # Show detailed progress indicator
436
+ spinner = Spinner("dots", text="[dim]Processing query...[/dim]")
437
+ live = Live(spinner, console=self.console, transient=True)
438
+ live.start()
439
+
440
+ try:
315
441
  request = ChatRequest(
316
442
  question=user_input,
317
443
  user_id="cli_user",
318
444
  conversation_id=self.session_id
319
445
  )
320
-
446
+
447
+ # Update spinner based on query type
448
+ if any(kw in user_input.lower() for kw in ['read', 'show', 'file', 'cat']):
449
+ spinner.update(text="[cyan]📄 Reading file...[/cyan]")
450
+ elif any(kw in user_input.lower() for kw in ['list', 'ls', 'find', 'search']):
451
+ spinner.update(text="[cyan]🔍 Searching files...[/cyan]")
452
+ elif any(kw in user_input.lower() for kw in ['python', 'calculate', 'run', 'execute']):
453
+ spinner.update(text="[cyan]⚙️ Executing code...[/cyan]")
454
+ elif any(kw in user_input.lower() for kw in ['research', 'paper', 'find', 'archive']):
455
+ spinner.update(text="[cyan]🔬 Searching research database...[/cyan]")
456
+ else:
457
+ spinner.update(text="[cyan]🤖 Thinking...[/cyan]")
458
+
321
459
  response = await self.agent.process_request(request)
460
+ finally:
461
+ live.stop()
322
462
 
323
463
  # Print response with typing effect for natural feel
324
464
  self.console.print("[bold violet]🤖 Agent[/]: ", end="", highlight=False)
325
465
 
326
- # Character-by-character streaming (like ChatGPT)
466
+ # Character-by-character streaming (like ChatGPT) - faster for long responses
327
467
  import time
328
468
  for char in response.response:
329
469
  self.console.print(char, end="", style="white")
330
- time.sleep(0.008) # 8ms per character (~125 chars/sec)
470
+ time.sleep(0.003) # 3ms per character (~333 chars/sec) - faster than before
331
471
 
332
472
  self.console.print() # Newline after response
333
473
 
@@ -732,6 +872,24 @@ Examples:
732
872
  action='store_true',
733
873
  help='Show recent query history'
734
874
  )
875
+
876
+ parser.add_argument(
877
+ '--presets',
878
+ action='store_true',
879
+ help='Show curated beta showcase prompts'
880
+ )
881
+
882
+ parser.add_argument(
883
+ '--metrics',
884
+ action='store_true',
885
+ help='Display the latest autonomy harness metrics summary'
886
+ )
887
+
888
+ parser.add_argument(
889
+ '--token-report',
890
+ action='store_true',
891
+ help='Print aggregated token usage from telemetry logs'
892
+ )
735
893
 
736
894
  parser.add_argument(
737
895
  '--search-library',
@@ -772,6 +930,21 @@ Examples:
772
930
  print("AI Research Assistant with real data integration")
773
931
  return
774
932
 
933
+ if args.presets:
934
+ cli = NocturnalCLI()
935
+ cli.show_presets()
936
+ return
937
+
938
+ if args.metrics:
939
+ cli = NocturnalCLI()
940
+ cli.show_metrics()
941
+ return
942
+
943
+ if args.token_report:
944
+ cli = NocturnalCLI()
945
+ cli.show_token_report()
946
+ return
947
+
775
948
  if args.tips or (args.query and args.query.lower() == "tips" and not args.interactive):
776
949
  cli = NocturnalCLI()
777
950
  cli.show_tips()
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env python3
2
+ """Lightweight persistent archive for conversation summaries."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import hashlib
8
+ import os
9
+ from dataclasses import dataclass
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from typing import List, Dict, Any, Optional
13
+
14
+
15
+ @dataclass
16
+ class ArchiveEntry:
17
+ """Serialized representation of a conversation turn summary."""
18
+
19
+ timestamp: str
20
+ question: str
21
+ summary: str
22
+ tools_used: List[str]
23
+ citations: Optional[List[str]] = None
24
+
25
+ def to_dict(self) -> Dict[str, Any]:
26
+ data: Dict[str, Any] = {
27
+ "timestamp": self.timestamp,
28
+ "question": self.question,
29
+ "summary": self.summary,
30
+ "tools_used": list(self.tools_used),
31
+ }
32
+ if self.citations:
33
+ data["citations"] = list(self.citations)
34
+ return data
35
+
36
+ @staticmethod
37
+ def from_dict(payload: Dict[str, Any]) -> "ArchiveEntry":
38
+ return ArchiveEntry(
39
+ timestamp=payload.get("timestamp", datetime.now(timezone.utc).isoformat()),
40
+ question=payload.get("question", ""),
41
+ summary=payload.get("summary", ""),
42
+ tools_used=list(payload.get("tools_used", [])),
43
+ citations=list(payload.get("citations", [])) or None,
44
+ )
45
+
46
+
47
+ class ConversationArchive:
48
+ """Stores compact conversation summaries for long-running research threads."""
49
+
50
+ def __init__(
51
+ self,
52
+ root: Optional[Path] = None,
53
+ enabled: Optional[bool] = None,
54
+ max_entries: int = 30,
55
+ ) -> None:
56
+ self.enabled = True if enabled is None else bool(enabled)
57
+ self.max_entries = max(1, max_entries)
58
+ env_root = os.getenv("CITE_AGENT_ARCHIVE_DIR")
59
+ final_root = root or Path(env_root) if env_root else root
60
+ self.root = Path(final_root or (Path.home() / ".cite_agent" / "conversation_archive"))
61
+ if self.enabled:
62
+ self.root.mkdir(parents=True, exist_ok=True)
63
+
64
+ @staticmethod
65
+ def _hash_identifier(identifier: str) -> str:
66
+ digest = hashlib.sha256(identifier.encode("utf-8")).hexdigest()
67
+ return digest[:16]
68
+
69
+ def _conversation_path(self, user_id: str, conversation_id: str) -> Path:
70
+ user_hash = self._hash_identifier(user_id or "anonymous")
71
+ convo_hash = self._hash_identifier(conversation_id or "default")
72
+ return self.root / f"{user_hash}-{convo_hash}.json"
73
+
74
+ def record_entry(
75
+ self,
76
+ user_id: str,
77
+ conversation_id: str,
78
+ question: str,
79
+ summary: str,
80
+ tools_used: Optional[List[str]] = None,
81
+ citations: Optional[List[str]] = None,
82
+ ) -> None:
83
+ if not self.enabled:
84
+ return
85
+
86
+ entry = ArchiveEntry(
87
+ timestamp=datetime.now(timezone.utc).isoformat(),
88
+ question=question.strip(),
89
+ summary=summary.strip(),
90
+ tools_used=list(tools_used or []),
91
+ citations=list(citations or []) or None,
92
+ )
93
+
94
+ path = self._conversation_path(user_id, conversation_id)
95
+ entries: List[ArchiveEntry] = []
96
+ if path.exists():
97
+ try:
98
+ data = json.loads(path.read_text(encoding="utf-8"))
99
+ entries = [ArchiveEntry.from_dict(item) for item in data]
100
+ except Exception:
101
+ entries = []
102
+
103
+ entries.append(entry)
104
+ entries = entries[-self.max_entries:]
105
+
106
+ serialized = [item.to_dict() for item in entries]
107
+ path.write_text(json.dumps(serialized, indent=2), encoding="utf-8")
108
+
109
+ def get_recent_context(
110
+ self,
111
+ user_id: str,
112
+ conversation_id: str,
113
+ limit: int = 3,
114
+ ) -> str:
115
+ if not self.enabled:
116
+ return ""
117
+
118
+ path = self._conversation_path(user_id, conversation_id)
119
+ if not path.exists():
120
+ return ""
121
+
122
+ try:
123
+ data = json.loads(path.read_text(encoding="utf-8"))
124
+ except Exception:
125
+ return ""
126
+
127
+ if not isinstance(data, list):
128
+ return ""
129
+
130
+ entries = [ArchiveEntry.from_dict(item) for item in data][-max(1, limit):]
131
+ if not entries:
132
+ return ""
133
+
134
+ lines = ["Archived context from previous sessions:"]
135
+ for item in entries:
136
+ snippet = item.summary.strip().replace("\n", " ")
137
+ if len(snippet) > 220:
138
+ snippet = snippet[:217].rstrip() + "..."
139
+ lines.append(f"• {item.timestamp[:19]} — {snippet}")
140
+ return "\n".join(lines)
141
+
142
+ def clear_conversation(self, user_id: str, conversation_id: str) -> None:
143
+ if not self.enabled:
144
+ return
145
+ path = self._conversation_path(user_id, conversation_id)
146
+ if path.exists():
147
+ path.unlink()
148
+
149
+ def list_conversations(self) -> List[str]:
150
+ if not self.enabled or not self.root.exists():
151
+ return []
152
+ return [p.name for p in self.root.glob("*.json")]