cite-agent 1.3.4__tar.gz → 1.4.3__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.
Files changed (153) hide show
  1. {cite_agent-1.3.4/cite_agent.egg-info → cite_agent-1.4.3}/PKG-INFO +15 -1
  2. {cite_agent-1.3.4 → cite_agent-1.4.3}/README.md +15 -1
  3. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/__init__.py +13 -13
  4. cite_agent-1.4.3/cite_agent/__version__.py +1 -0
  5. cite_agent-1.4.3/cite_agent/action_first_mode.py +150 -0
  6. cite_agent-1.4.3/cite_agent/adaptive_providers.py +413 -0
  7. cite_agent-1.4.3/cite_agent/archive_api_client.py +186 -0
  8. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/auth.py +0 -1
  9. cite_agent-1.4.3/cite_agent/auto_expander.py +70 -0
  10. cite_agent-1.4.3/cite_agent/cache.py +379 -0
  11. cite_agent-1.4.3/cite_agent/circuit_breaker.py +370 -0
  12. cite_agent-1.4.3/cite_agent/citation_network.py +377 -0
  13. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/cli.py +198 -13
  14. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/cli_conversational.py +113 -3
  15. cite_agent-1.4.3/cite_agent/confidence_calibration.py +381 -0
  16. cite_agent-1.4.3/cite_agent/conversation_archive.py +152 -0
  17. cite_agent-1.4.3/cite_agent/deduplication.py +325 -0
  18. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/enhanced_ai_agent.py +2438 -628
  19. cite_agent-1.4.3/cite_agent/error_handler.py +228 -0
  20. cite_agent-1.4.3/cite_agent/execution_safety.py +329 -0
  21. cite_agent-1.4.3/cite_agent/full_paper_reader.py +239 -0
  22. cite_agent-1.4.3/cite_agent/observability.py +398 -0
  23. cite_agent-1.4.3/cite_agent/offline_mode.py +348 -0
  24. cite_agent-1.4.3/cite_agent/paper_comparator.py +368 -0
  25. cite_agent-1.4.3/cite_agent/paper_summarizer.py +420 -0
  26. cite_agent-1.4.3/cite_agent/pdf_extractor.py +350 -0
  27. cite_agent-1.4.3/cite_agent/proactive_boundaries.py +266 -0
  28. cite_agent-1.4.3/cite_agent/project_detector.py +148 -0
  29. cite_agent-1.4.3/cite_agent/quality_gate.py +442 -0
  30. cite_agent-1.4.3/cite_agent/request_queue.py +390 -0
  31. cite_agent-1.4.3/cite_agent/response_enhancer.py +257 -0
  32. cite_agent-1.4.3/cite_agent/response_formatter.py +458 -0
  33. cite_agent-1.4.3/cite_agent/response_pipeline.py +295 -0
  34. cite_agent-1.4.3/cite_agent/response_style_enhancer.py +259 -0
  35. cite_agent-1.4.3/cite_agent/self_healing.py +418 -0
  36. cite_agent-1.4.3/cite_agent/similarity_finder.py +524 -0
  37. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/streaming_ui.py +13 -9
  38. cite_agent-1.4.3/cite_agent/thinking_blocks.py +308 -0
  39. cite_agent-1.4.3/cite_agent/tool_orchestrator.py +416 -0
  40. cite_agent-1.4.3/cite_agent/trend_analyzer.py +540 -0
  41. cite_agent-1.4.3/cite_agent/unpaywall_client.py +226 -0
  42. {cite_agent-1.3.4 → cite_agent-1.4.3/cite_agent.egg-info}/PKG-INFO +15 -1
  43. cite_agent-1.4.3/cite_agent.egg-info/SOURCES.txt +120 -0
  44. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent.egg-info/top_level.txt +0 -1
  45. cite_agent-1.4.3/docs/AGENT_INTELLIGENCE_REPORT.md +159 -0
  46. {cite_agent-1.3.4 → cite_agent-1.4.3}/docs/BETA_LAUNCH_CHECKLIST.md +2 -0
  47. cite_agent-1.4.3/docs/BETA_PITCH_v1.3.9.md +95 -0
  48. cite_agent-1.4.3/docs/BETA_SHOWCASE_GUIDE.md +43 -0
  49. cite_agent-1.4.3/docs/CONSISTENCY_TEST_RESULTS.md +426 -0
  50. cite_agent-1.4.3/docs/CONVERSATIONAL_DEPTH_REPORT.md +237 -0
  51. cite_agent-1.4.3/docs/DEV_NOTES_2025-10-30.md +8 -0
  52. cite_agent-1.4.3/docs/FINAL_TEST_RESULTS_CEREBRAS.md +416 -0
  53. cite_agent-1.4.3/docs/HONEST_TESTING_ASSESSMENT.md +339 -0
  54. cite_agent-1.4.3/docs/MAXIMUM_CAPABILITY_ACHIEVED.md +308 -0
  55. cite_agent-1.4.3/docs/TEST_RESULTS_2025-11-01.md +161 -0
  56. cite_agent-1.4.3/docs/TEST_RESULTS_SESSION_2025-11-04.md +195 -0
  57. cite_agent-1.4.3/docs/TEST_RUN_2025-11-01.md +259 -0
  58. cite_agent-1.4.3/docs/TEST_RUN_ANALYSIS_CEREBRAS.md +411 -0
  59. cite_agent-1.4.3/docs/TEST_RUN_UPDATE_2025-11-01.md +217 -0
  60. {cite_agent-1.3.4 → cite_agent-1.4.3}/setup.py +1 -1
  61. cite_agent-1.4.3/tests/enhanced/conftest.py +11 -0
  62. {cite_agent-1.3.4 → cite_agent-1.4.3}/tests/enhanced/test_account_client.py +6 -5
  63. {cite_agent-1.3.4 → cite_agent-1.4.3}/tests/enhanced/test_archive_agent.py +30 -16
  64. cite_agent-1.4.3/tests/enhanced/test_autonomy_harness.py +124 -0
  65. cite_agent-1.4.3/tests/enhanced/test_conversation_archive.py +41 -0
  66. {cite_agent-1.3.4 → cite_agent-1.4.3}/tests/enhanced/test_enhanced_agent_runtime.py +15 -64
  67. cite_agent-1.4.3/tests/enhanced/test_financial_planner.py +59 -0
  68. {cite_agent-1.3.4 → cite_agent-1.4.3}/tests/enhanced/test_setup_config.py +1 -5
  69. {cite_agent-1.3.4 → cite_agent-1.4.3}/tests/integration_test.py +12 -12
  70. cite_agent-1.4.3/tests/test_action_first_mode.py +129 -0
  71. cite_agent-1.4.3/tests/test_end_to_end.py +165 -0
  72. cite_agent-1.4.3/tests/test_new_features.py +410 -0
  73. cite_agent-1.4.3/tests/test_proactive_boundaries.py +201 -0
  74. cite_agent-1.4.3/tests/test_real_world_scenarios.py +422 -0
  75. cite_agent-1.4.3/tests/test_robustness_comprehensive.py +339 -0
  76. cite_agent-1.4.3/tests/test_style_enhancement.py +113 -0
  77. cite_agent-1.4.3/tests/test_style_with_mock.py +157 -0
  78. cite_agent-1.3.4/cite_agent/__version__.py +0 -1
  79. cite_agent-1.3.4/cite_agent.egg-info/SOURCES.txt +0 -91
  80. cite_agent-1.3.4/src/__init__.py +0 -1
  81. cite_agent-1.3.4/src/services/__init__.py +0 -132
  82. cite_agent-1.3.4/src/services/auth_service/__init__.py +0 -3
  83. cite_agent-1.3.4/src/services/auth_service/auth_manager.py +0 -33
  84. cite_agent-1.3.4/src/services/graph/__init__.py +0 -1
  85. cite_agent-1.3.4/src/services/graph/knowledge_graph.py +0 -194
  86. cite_agent-1.3.4/src/services/llm_service/__init__.py +0 -5
  87. cite_agent-1.3.4/src/services/llm_service/llm_manager.py +0 -495
  88. cite_agent-1.3.4/src/services/paper_service/__init__.py +0 -5
  89. cite_agent-1.3.4/src/services/paper_service/openalex.py +0 -231
  90. cite_agent-1.3.4/src/services/performance_service/__init__.py +0 -1
  91. cite_agent-1.3.4/src/services/performance_service/rust_performance.py +0 -395
  92. cite_agent-1.3.4/src/services/research_service/__init__.py +0 -23
  93. cite_agent-1.3.4/src/services/research_service/chatbot.py +0 -2056
  94. cite_agent-1.3.4/src/services/research_service/citation_manager.py +0 -436
  95. cite_agent-1.3.4/src/services/research_service/context_manager.py +0 -1441
  96. cite_agent-1.3.4/src/services/research_service/conversation_manager.py +0 -597
  97. cite_agent-1.3.4/src/services/research_service/critical_paper_detector.py +0 -577
  98. cite_agent-1.3.4/src/services/research_service/enhanced_research.py +0 -121
  99. cite_agent-1.3.4/src/services/research_service/enhanced_synthesizer.py +0 -375
  100. cite_agent-1.3.4/src/services/research_service/query_generator.py +0 -777
  101. cite_agent-1.3.4/src/services/research_service/synthesizer.py +0 -1273
  102. cite_agent-1.3.4/src/services/search_service/__init__.py +0 -5
  103. cite_agent-1.3.4/src/services/search_service/indexer.py +0 -186
  104. cite_agent-1.3.4/src/services/search_service/search_engine.py +0 -342
  105. cite_agent-1.3.4/src/services/simple_enhanced_main.py +0 -287
  106. cite_agent-1.3.4/tests/beta_launch_test_suite.py +0 -369
  107. cite_agent-1.3.4/tests/test_truth_seeking_comprehensive.py +0 -399
  108. cite_agent-1.3.4/tools/security_audit.py +0 -149
  109. {cite_agent-1.3.4 → cite_agent-1.4.3}/LICENSE +0 -0
  110. {cite_agent-1.3.4 → cite_agent-1.4.3}/MANIFEST.in +0 -0
  111. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/__main__.py +0 -0
  112. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/account_client.py +0 -0
  113. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/agent_backend_only.py +0 -0
  114. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/ascii_plotting.py +0 -0
  115. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/backend_only_client.py +0 -0
  116. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/cli_enhanced.py +0 -0
  117. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/cli_workflow.py +0 -0
  118. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/dashboard.py +0 -0
  119. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/rate_limiter.py +0 -0
  120. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/session_manager.py +0 -0
  121. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/setup_config.py +0 -0
  122. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/telemetry.py +0 -0
  123. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/ui.py +0 -0
  124. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/updater.py +0 -0
  125. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/web_search.py +0 -0
  126. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/workflow.py +0 -0
  127. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent/workflow_integration.py +0 -0
  128. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent.egg-info/dependency_links.txt +0 -0
  129. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent.egg-info/entry_points.txt +0 -0
  130. {cite_agent-1.3.4 → cite_agent-1.4.3}/cite_agent.egg-info/requires.txt +0 -0
  131. {cite_agent-1.3.4 → cite_agent-1.4.3}/docs/BETA_RELEASE_CHECKLIST.md +0 -0
  132. {cite_agent-1.3.4 → cite_agent-1.4.3}/docs/ENHANCED_CAPABILITIES.md +0 -0
  133. {cite_agent-1.3.4 → cite_agent-1.4.3}/docs/GROQ_RATE_LIMITS.md +0 -0
  134. {cite_agent-1.3.4 → cite_agent-1.4.3}/docs/INSTALL.md +0 -0
  135. {cite_agent-1.3.4 → cite_agent-1.4.3}/docs/PUBLISHING_PYPI.md +0 -0
  136. {cite_agent-1.3.4 → cite_agent-1.4.3}/docs/SECURE_PACKAGING_GUIDE.md +0 -0
  137. {cite_agent-1.3.4 → cite_agent-1.4.3}/docs/SECURITY_AUDIT.md +0 -0
  138. {cite_agent-1.3.4 → cite_agent-1.4.3}/docs/USER_GETTING_STARTED.md +0 -0
  139. {cite_agent-1.3.4 → cite_agent-1.4.3}/docs/playbooks/BETA_LAUNCH_PLAYBOOK.md +0 -0
  140. {cite_agent-1.3.4 → cite_agent-1.4.3}/requirements.txt +0 -0
  141. {cite_agent-1.3.4 → cite_agent-1.4.3}/setup.cfg +0 -0
  142. {cite_agent-1.3.4 → cite_agent-1.4.3}/tests/enhanced/test_reasoning_engine.py +0 -0
  143. {cite_agent-1.3.4 → cite_agent-1.4.3}/tests/enhanced/test_tool_framework.py +0 -0
  144. {cite_agent-1.3.4 → cite_agent-1.4.3}/tests/validation/test_accuracy_system.py +0 -0
  145. {cite_agent-1.3.4 → cite_agent-1.4.3}/tests/validation/test_agent_live.py +0 -0
  146. {cite_agent-1.3.4 → cite_agent-1.4.3}/tests/validation/test_backend_local.py +0 -0
  147. {cite_agent-1.3.4 → cite_agent-1.4.3}/tests/validation/test_cerebras_comparison.py +0 -0
  148. {cite_agent-1.3.4 → cite_agent-1.4.3}/tests/validation/test_improved_prompt.py +0 -0
  149. {cite_agent-1.3.4 → cite_agent-1.4.3}/tests/validation/test_qualitative_robustness.py +0 -0
  150. {cite_agent-1.3.4 → cite_agent-1.4.3}/tests/validation/test_qualitative_system.py +0 -0
  151. {cite_agent-1.3.4 → cite_agent-1.4.3}/tests/validation/test_truth_seeking_chinese.py +0 -0
  152. {cite_agent-1.3.4 → cite_agent-1.4.3}/tests/validation/test_truth_seeking_real.py +0 -0
  153. /cite_agent-1.3.4/tests/validation/test_truth_seeking_comprehensive.py → /cite_agent-1.4.3/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.4
3
+ Version: 1.4.3
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
@@ -194,6 +194,11 @@ cite-agent "我的p值是0.05,這顯著嗎?"
194
194
  cite-agent "天空是藍色的嗎?"
195
195
  ```
196
196
 
197
+ #### Runtime Controls
198
+
199
+ - Responses render immediately—there’s no artificial typing delay.
200
+ - Press `Ctrl+C` while the agent is thinking or streaming to interrupt and ask a different question on the spot.
201
+
197
202
  ### Python API Reference
198
203
 
199
204
  #### EnhancedNocturnalAgent
@@ -436,6 +441,15 @@ export NOCTURNAL_DEBUG=1
436
441
  cite-agent "your query"
437
442
  ```
438
443
 
444
+ ### Documentation
445
+
446
+ For developers and contributors:
447
+ - **[SYSTEM_STATUS.md](SYSTEM_STATUS.md)** - Current system status, test results, how to run
448
+ - **[ARCHITECTURE_EXPLAINED.md](ARCHITECTURE_EXPLAINED.md)** - Why the codebase is complex
449
+ - **[CHANGELOG.md](CHANGELOG.md)** - Version history and changes
450
+ - **[INSTALL.md](INSTALL.md)** - Detailed installation instructions
451
+ - **[TESTING.md](TESTING.md)** - How to run tests
452
+
439
453
  ### Support
440
454
 
441
455
  - **Documentation**: [Full docs](https://docs.cite-agent.com)
@@ -157,6 +157,11 @@ cite-agent "我的p值是0.05,這顯著嗎?"
157
157
  cite-agent "天空是藍色的嗎?"
158
158
  ```
159
159
 
160
+ #### Runtime Controls
161
+
162
+ - Responses render immediately—there’s no artificial typing delay.
163
+ - Press `Ctrl+C` while the agent is thinking or streaming to interrupt and ask a different question on the spot.
164
+
160
165
  ### Python API Reference
161
166
 
162
167
  #### EnhancedNocturnalAgent
@@ -399,6 +404,15 @@ export NOCTURNAL_DEBUG=1
399
404
  cite-agent "your query"
400
405
  ```
401
406
 
407
+ ### Documentation
408
+
409
+ For developers and contributors:
410
+ - **[SYSTEM_STATUS.md](SYSTEM_STATUS.md)** - Current system status, test results, how to run
411
+ - **[ARCHITECTURE_EXPLAINED.md](ARCHITECTURE_EXPLAINED.md)** - Why the codebase is complex
412
+ - **[CHANGELOG.md](CHANGELOG.md)** - Version history and changes
413
+ - **[INSTALL.md](INSTALL.md)** - Detailed installation instructions
414
+ - **[TESTING.md](TESTING.md)** - How to run tests
415
+
402
416
  ### Support
403
417
 
404
418
  - **Documentation**: [Full docs](https://docs.cite-agent.com)
@@ -427,4 +441,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
427
441
 
428
442
  ---
429
443
 
430
- **Made with ❤️ for the research community**
444
+ **Made with ❤️ for the research community**
@@ -7,9 +7,9 @@ prior stacks preserved only in Git history, kept out of the runtime footprint.
7
7
 
8
8
  from .enhanced_ai_agent import EnhancedNocturnalAgent, ChatRequest, ChatResponse
9
9
 
10
- __version__ = "0.9.0b1"
11
- __author__ = "Nocturnal Archive Team"
12
- __email__ = "contact@nocturnal.dev"
10
+ __version__ = "1.4.0"
11
+ __author__ = "Cite Agent Team"
12
+ __email__ = "contact@citeagent.dev"
13
13
 
14
14
  __all__ = [
15
15
  "EnhancedNocturnalAgent",
@@ -18,10 +18,10 @@ __all__ = [
18
18
  ]
19
19
 
20
20
  # Package metadata
21
- PACKAGE_NAME = "nocturnal-archive"
21
+ PACKAGE_NAME = "cite-agent"
22
22
  PACKAGE_VERSION = __version__
23
- PACKAGE_DESCRIPTION = "Beta CLI agent for finance + research workflows"
24
- PACKAGE_URL = "https://github.com/Spectating101/nocturnal-archive"
23
+ PACKAGE_DESCRIPTION = "Research and finance CLI copilot with shell, Archive, and FinSight tools"
24
+ PACKAGE_URL = "https://github.com/Spectating101/cite-agent"
25
25
 
26
26
  def get_version():
27
27
  """Get the package version"""
@@ -30,22 +30,22 @@ def get_version():
30
30
  def quick_start():
31
31
  """Print quick start instructions"""
32
32
  print("""
33
- 🚀 Nocturnal Archive Quick Start
34
- ================================
33
+ 🚀 Cite Agent Quick Start
34
+ =========================
35
35
 
36
36
  1. Install the package and CLI:
37
- pip install nocturnal-archive
37
+ pip install cite-agent
38
38
 
39
- 2. Configure your Groq key:
40
- nocturnal --setup
39
+ 2. Configure your account or local keys:
40
+ cite-agent --setup
41
41
 
42
42
  3. Ask a question:
43
- nocturnal "Compare Apple and Microsoft net income this quarter"
43
+ cite-agent "Compare Apple and Microsoft net income this quarter"
44
44
 
45
45
  4. Prefer embedding in code? Minimal example:
46
46
  ```python
47
47
  import asyncio
48
- from nocturnal_archive import EnhancedNocturnalAgent, ChatRequest
48
+ from cite_agent import EnhancedNocturnalAgent, ChatRequest
49
49
 
50
50
  async def main():
51
51
  agent = EnhancedNocturnalAgent()
@@ -0,0 +1 @@
1
+ __version__ = "1.4.1"
@@ -0,0 +1,150 @@
1
+ """
2
+ Action-First Response Mode
3
+
4
+ Makes agent SHOW results proactively instead of just talking about them
5
+
6
+ Key principles:
7
+ 1. DO the obvious next step without asking
8
+ 2. SHOW data, don't just describe it
9
+ 3. Less talk, more action
10
+ 4. Proactive, not reactive
11
+ """
12
+
13
+ import logging
14
+ from typing import Dict, Any, List
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class ActionFirstMode:
20
+ """
21
+ Transforms agent from conversation-first to action-first
22
+
23
+ BEFORE (conversation-first):
24
+ User: "List Python files"
25
+ Agent: "I found 3 files. Want me to show you what's in them?"
26
+
27
+ AFTER (action-first):
28
+ User: "List Python files"
29
+ Agent: [Shows list + Shows preview of main file + Shows key functions]
30
+ """
31
+
32
+ @classmethod
33
+ def should_auto_expand(cls, query: str, response: str, context: Dict[str, Any]) -> bool:
34
+ """
35
+ Determine if agent should automatically show more detail
36
+
37
+ Returns True if:
38
+ - Listed files → should preview main one
39
+ - Listed papers → should show abstracts
40
+ - Listed code → should show key functions
41
+ - Found data → should show sample
42
+ """
43
+ query_lower = query.lower()
44
+ response_lower = response.lower()
45
+
46
+ # If response just lists things without details, should expand
47
+ if any(word in response_lower for word in ['found', 'here are', 'listed']):
48
+ # Check if it's a list without details
49
+ has_bullets = '•' in response or '\n-' in response
50
+ is_short = len(response) < 300
51
+
52
+ if has_bullets and is_short:
53
+ return True
54
+
55
+ # If listing files, should preview
56
+ if any(word in query_lower for word in ['list', 'show', 'find']) and \
57
+ any(word in query_lower for word in ['file', 'files', 'code']):
58
+ return True
59
+
60
+ # If finding papers, should show abstracts
61
+ if 'papers' in query_lower or 'research' in query_lower:
62
+ return True
63
+
64
+ # If data query, should show sample
65
+ if any(word in query_lower for word in ['data', 'revenue', 'metrics', 'stats']):
66
+ return True
67
+
68
+ return False
69
+
70
+ @classmethod
71
+ def get_auto_expansion_prompt(cls, query: str, initial_response: str, context: Dict[str, Any]) -> str:
72
+ """
73
+ Generate prompt for automatic expansion
74
+
75
+ This tells the agent to SHOW more detail proactively
76
+ """
77
+ query_lower = query.lower()
78
+
79
+ # File listing → preview main file
80
+ if 'file' in query_lower and any(ext in initial_response for ext in ['.py', '.js', '.md']):
81
+ return "Now show a preview (first 50 lines) of the most important file automatically. Don't ask - just show it."
82
+
83
+ # Papers → show abstracts
84
+ if 'paper' in query_lower:
85
+ return "Now show the abstract/summary of the top 2-3 papers automatically. Don't ask - just show them."
86
+
87
+ # Code → show key functions
88
+ if 'code' in query_lower or 'function' in query_lower:
89
+ return "Now show the key functions/classes in the main file automatically. Don't ask - just show them."
90
+
91
+ # Data → show sample
92
+ if 'data' in query_lower or 'revenue' in query_lower:
93
+ return "Now show a sample/visualization of the data automatically. Don't ask - just show it."
94
+
95
+ return "Show the most useful additional detail automatically without asking permission."
96
+
97
+ @classmethod
98
+ def remove_asking_phrases(cls, response: str) -> str:
99
+ """
100
+ Remove phrases that ASK instead of DO
101
+
102
+ "Want me to..." → Just do it
103
+ "Should I..." → Just do it
104
+ "Would you like..." → Just do it
105
+ """
106
+ import re
107
+
108
+ asking_patterns = [
109
+ r'Want me to[^?]+\?',
110
+ r'Should I[^?]+\?',
111
+ r'Would you like[^?]+\?',
112
+ r'Need me to[^?]+\?',
113
+ r'Let me know if you want me to[^.]+\.',
114
+ r'Let me know if you need[^.]+\.',
115
+ ]
116
+
117
+ cleaned = response
118
+ for pattern in asking_patterns:
119
+ cleaned = re.sub(pattern, '', cleaned, flags=re.IGNORECASE)
120
+
121
+ # Clean up extra whitespace/newlines
122
+ cleaned = re.sub(r'\n\n+', '\n\n', cleaned)
123
+ cleaned = cleaned.strip()
124
+
125
+ return cleaned
126
+
127
+ @classmethod
128
+ def make_action_first(cls, response: str, query: str, context: Dict[str, Any]) -> str:
129
+ """
130
+ Transform response to be action-first
131
+
132
+ 1. Remove asking phrases
133
+ 2. If response is just a list, it should have been auto-expanded
134
+ 3. Focus on SHOWING not TELLING
135
+ """
136
+ # Remove asking phrases
137
+ action_response = cls.remove_asking_phrases(response)
138
+
139
+ # If response is still too conversation-heavy, make it data-heavy
140
+ if len(action_response) < 200 and not any(marker in action_response for marker in ['```', '•', '\n-']):
141
+ # Response is short and has no data structure - flag for expansion
142
+ logger.warning("Response is conversation-heavy, should be more action-first")
143
+
144
+ return action_response
145
+
146
+
147
+ # Convenience function
148
+ def make_action_first(response: str, query: str, context: Dict[str, Any] = None) -> str:
149
+ """Quick action-first transformation"""
150
+ return ActionFirstMode.make_action_first(response, query, context or {})
@@ -0,0 +1,413 @@
1
+ """
2
+ Adaptive Provider Selection System
3
+ Learns which provider is best for different query types and auto-switches
4
+ """
5
+
6
+ from dataclasses import dataclass, field
7
+ from enum import Enum
8
+ from typing import Dict, List, Optional, Tuple
9
+ from datetime import datetime, timedelta
10
+ import logging
11
+ import json
12
+ from pathlib import Path
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class QueryType(Enum):
18
+ """Categories of queries handled"""
19
+ ACADEMIC_PAPER = "academic_paper" # Paper search, citations
20
+ FINANCIAL_DATA = "financial_data" # Stock prices, metrics
21
+ WEB_SEARCH = "web_search" # General web search
22
+ CODE_GENERATION = "code_generation" # Write/debug code
23
+ DATA_ANALYSIS = "data_analysis" # CSV, statistical analysis
24
+ CONVERSATION = "conversation" # General chat
25
+ SHELL_EXECUTION = "shell_execution" # System commands
26
+
27
+
28
+ @dataclass
29
+ class ProviderPerformanceProfile:
30
+ """Performance metrics for a provider on a specific query type"""
31
+ provider_name: str
32
+ query_type: QueryType
33
+ total_requests: int = 0
34
+ successful_requests: int = 0
35
+ avg_latency_ms: float = 0.0
36
+ p95_latency_ms: float = 0.0
37
+ accuracy_score: float = 1.0 # 0.0 to 1.0
38
+ cost_per_request: float = 0.0
39
+ last_used: Optional[datetime] = None
40
+ latency_history: List[float] = field(default_factory=list)
41
+ error_history: List[str] = field(default_factory=list)
42
+
43
+ def get_success_rate(self) -> float:
44
+ """Success rate for this provider on this query type"""
45
+ if self.total_requests == 0:
46
+ return 0.0
47
+ return self.successful_requests / self.total_requests
48
+
49
+ def get_score(self) -> float:
50
+ """Composite score for this provider (higher is better)"""
51
+ success_rate = self.get_success_rate()
52
+ latency_penalty = min(self.avg_latency_ms / 1000, 10) # Cap at 10s penalty
53
+ cost_penalty = self.cost_per_request * 100 # Cost in cents
54
+
55
+ # Score = (success_rate * accuracy) - latency_penalty - cost_penalty
56
+ score = (success_rate * self.accuracy_score * 100) - latency_penalty - cost_penalty
57
+ return max(0, score) # Never negative
58
+
59
+ def add_result(
60
+ self,
61
+ success: bool,
62
+ latency_ms: float,
63
+ accuracy_score: float = 1.0,
64
+ cost: float = 0.0,
65
+ error: Optional[str] = None
66
+ ):
67
+ """Record a result for this provider"""
68
+ self.total_requests += 1
69
+ self.latency_history.append(latency_ms)
70
+
71
+ # Keep only last 100 latencies for p95 calculation
72
+ if len(self.latency_history) > 100:
73
+ self.latency_history = self.latency_history[-100:]
74
+
75
+ # Update p95 latency
76
+ sorted_latencies = sorted(self.latency_history)
77
+ idx = int(len(sorted_latencies) * 0.95)
78
+ self.p95_latency_ms = sorted_latencies[min(idx, len(sorted_latencies) - 1)]
79
+
80
+ # Update average
81
+ self.avg_latency_ms = sum(self.latency_history) / len(self.latency_history)
82
+
83
+ if success:
84
+ self.successful_requests += 1
85
+ self.accuracy_score = (self.accuracy_score + accuracy_score) / 2
86
+ else:
87
+ self.error_history.append(error or "unknown")
88
+ # Keep last 10 errors
89
+ if len(self.error_history) > 10:
90
+ self.error_history = self.error_history[-10:]
91
+
92
+ self.cost_per_request = cost
93
+ self.last_used = datetime.now()
94
+
95
+
96
+ @dataclass
97
+ class ProviderSelectionPolicy:
98
+ """Policy for selecting providers"""
99
+ always_prefer: Optional[str] = None # Force specific provider (e.g., "cerebras")
100
+ avoid_providers: List[str] = field(default_factory=list) # Never use these
101
+ cost_sensitive: bool = False # Prefer cheaper if performance similar
102
+ latency_sensitive: bool = True # Prefer faster
103
+ reliability_weight: float = 0.7 # How much to weight success rate
104
+ latency_weight: float = 0.3 # How much to weight latency
105
+
106
+
107
+ class AdaptiveProviderSelector:
108
+ """
109
+ Intelligently selects providers based on:
110
+ - Query type (different providers excel at different tasks)
111
+ - Historical performance (learns what works)
112
+ - Current system state (avoid degraded providers)
113
+ - User preferences (cost vs speed)
114
+ - Time of day (some providers have peak hours)
115
+ """
116
+
117
+ def __init__(self, storage_dir: Optional[Path] = None):
118
+ self.storage_dir = storage_dir or Path.home() / ".nocturnal_archive" / "provider_selection"
119
+ self.storage_dir.mkdir(parents=True, exist_ok=True)
120
+
121
+ # Performance profiles: provider -> query_type -> profile
122
+ self.profiles: Dict[str, Dict[QueryType, ProviderPerformanceProfile]] = {}
123
+
124
+ # Provider health status
125
+ self.provider_health: Dict[str, float] = {} # provider -> health (0.0-1.0)
126
+ self.provider_last_degraded: Dict[str, datetime] = {}
127
+
128
+ # Load historical data
129
+ self._load_profiles()
130
+
131
+ def select_provider(
132
+ self,
133
+ query_type: QueryType,
134
+ available_providers: List[str],
135
+ policy: Optional[ProviderSelectionPolicy] = None,
136
+ exclude: Optional[List[str]] = None
137
+ ) -> Tuple[str, Optional[str]]:
138
+ """
139
+ Select best provider for this query type
140
+
141
+ Returns:
142
+ (provider_name, fallback_provider)
143
+ """
144
+ policy = policy or ProviderSelectionPolicy()
145
+ exclude = exclude or []
146
+
147
+ # Filter available providers
148
+ candidates = [
149
+ p for p in available_providers
150
+ if p not in exclude and p not in policy.avoid_providers
151
+ ]
152
+
153
+ if not candidates:
154
+ # Fall back to anything available
155
+ candidates = available_providers
156
+
157
+ # If policy says use specific provider, use it
158
+ if policy.always_prefer and policy.always_prefer in candidates:
159
+ fallback = next((p for p in candidates if p != policy.always_prefer), None)
160
+ return policy.always_prefer, fallback
161
+
162
+ # Score each candidate
163
+ scores = {}
164
+ for provider in candidates:
165
+ profile = self._get_or_create_profile(provider, query_type)
166
+ health = self.provider_health.get(provider, 1.0)
167
+
168
+ # Composite score
169
+ score = profile.get_score() * health
170
+ scores[provider] = score
171
+
172
+ logger.debug(
173
+ f"Provider '{provider}' for {query_type.value}: "
174
+ f"score={profile.get_score():.1f}, health={health:.1%}"
175
+ )
176
+
177
+ # Select top 2
178
+ sorted_providers = sorted(scores.items(), key=lambda x: x[1], reverse=True)
179
+
180
+ if sorted_providers:
181
+ best = sorted_providers[0][0]
182
+ fallback = sorted_providers[1][0] if len(sorted_providers) > 1 else None
183
+
184
+ logger.info(
185
+ f"📊 Selected provider '{best}' for {query_type.value} "
186
+ f"(fallback: {fallback})"
187
+ )
188
+
189
+ return best, fallback
190
+
191
+ # Emergency fallback
192
+ return candidates[0] if candidates else "cerebras", None
193
+
194
+ def record_result(
195
+ self,
196
+ provider: str,
197
+ query_type: QueryType,
198
+ success: bool,
199
+ latency_ms: float,
200
+ accuracy_score: float = 1.0,
201
+ cost: float = 0.0,
202
+ error: Optional[str] = None
203
+ ):
204
+ """Record result of using a provider for a query type"""
205
+ profile = self._get_or_create_profile(provider, query_type)
206
+ profile.add_result(success, latency_ms, accuracy_score, cost, error)
207
+
208
+ # Update provider health based on success
209
+ current_health = self.provider_health.get(provider, 1.0)
210
+ if success:
211
+ # Improve health (back toward 1.0)
212
+ new_health = min(1.0, current_health + 0.05)
213
+ else:
214
+ # Degrade health
215
+ new_health = max(0.0, current_health - 0.1)
216
+
217
+ self.provider_health[provider] = new_health
218
+
219
+ if new_health < 0.5:
220
+ self.provider_last_degraded[provider] = datetime.now()
221
+ logger.warning(f"⚠️ Provider '{provider}' degraded (health: {new_health:.1%})")
222
+
223
+ # Save updated profiles
224
+ self._save_profiles()
225
+
226
+ def get_provider_recommendation(
227
+ self,
228
+ query_type: QueryType,
229
+ available_providers: List[str]
230
+ ) -> Dict[str, any]:
231
+ """Get detailed recommendation for a query type"""
232
+ recommendations = {}
233
+
234
+ for provider in available_providers:
235
+ profile = self._get_or_create_profile(provider, query_type)
236
+ health = self.provider_health.get(provider, 1.0)
237
+
238
+ recommendations[provider] = {
239
+ "score": profile.get_score(),
240
+ "success_rate": profile.get_success_rate(),
241
+ "avg_latency_ms": profile.avg_latency_ms,
242
+ "p95_latency_ms": profile.p95_latency_ms,
243
+ "requests_used": profile.total_requests,
244
+ "health": health,
245
+ "recommendation": "✅ Excellent" if profile.get_score() > 80 else
246
+ "✓ Good" if profile.get_score() > 50 else
247
+ "⚠️ Fair" if profile.get_score() > 20 else
248
+ "❌ Poor"
249
+ }
250
+
251
+ return recommendations
252
+
253
+ def get_provider_rankings(self, query_type: QueryType) -> List[Tuple[str, float]]:
254
+ """Rank providers for a specific query type"""
255
+ rankings = []
256
+
257
+ for provider, profiles_by_type in self.profiles.items():
258
+ if query_type in profiles_by_type:
259
+ profile = profiles_by_type[query_type]
260
+ health = self.provider_health.get(provider, 1.0)
261
+ score = profile.get_score() * health
262
+ rankings.append((provider, score))
263
+
264
+ return sorted(rankings, key=lambda x: x[1], reverse=True)
265
+
266
+ def should_switch_provider(
267
+ self,
268
+ current_provider: str,
269
+ query_type: QueryType
270
+ ) -> Tuple[bool, Optional[str]]:
271
+ """
272
+ Check if we should switch from current provider
273
+
274
+ Returns:
275
+ (should_switch, better_provider)
276
+ """
277
+ current_profile = self._get_or_create_profile(current_provider, query_type)
278
+ current_health = self.provider_health.get(current_provider, 1.0)
279
+ current_score = current_profile.get_score() * current_health
280
+
281
+ # If health is very low, definitely switch
282
+ if current_health < 0.3:
283
+ rankings = self.get_provider_rankings(query_type)
284
+ if rankings and rankings[0][0] != current_provider:
285
+ return True, rankings[0][0]
286
+
287
+ # If there's a significantly better option, switch
288
+ rankings = self.get_provider_rankings(query_type)
289
+ for provider, score in rankings[:3]:
290
+ if provider != current_provider and score > current_score * 1.2:
291
+ return True, provider
292
+
293
+ return False, None
294
+
295
+ def _get_or_create_profile(
296
+ self,
297
+ provider: str,
298
+ query_type: QueryType
299
+ ) -> ProviderPerformanceProfile:
300
+ """Get or create performance profile"""
301
+ if provider not in self.profiles:
302
+ self.profiles[provider] = {}
303
+
304
+ if query_type not in self.profiles[provider]:
305
+ self.profiles[provider][query_type] = ProviderPerformanceProfile(
306
+ provider_name=provider,
307
+ query_type=query_type
308
+ )
309
+
310
+ return self.profiles[provider][query_type]
311
+
312
+ def _load_profiles(self):
313
+ """Load historical performance data"""
314
+ profile_file = self.storage_dir / "provider_profiles.json"
315
+ if not profile_file.exists():
316
+ return
317
+
318
+ try:
319
+ with open(profile_file, 'r') as f:
320
+ data = json.load(f)
321
+
322
+ for provider, query_types in data.items():
323
+ for query_type_str, profile_data in query_types.items():
324
+ try:
325
+ query_type = QueryType(query_type_str)
326
+ profile = ProviderPerformanceProfile(**profile_data)
327
+
328
+ if provider not in self.profiles:
329
+ self.profiles[provider] = {}
330
+ self.profiles[provider][query_type] = profile
331
+ except ValueError:
332
+ continue
333
+
334
+ logger.info(f"📥 Loaded {len(self.profiles)} provider profiles")
335
+
336
+ except Exception as e:
337
+ logger.error(f"Failed to load profiles: {e}")
338
+
339
+ def _save_profiles(self):
340
+ """Save performance data to disk"""
341
+ try:
342
+ profile_file = self.storage_dir / "provider_profiles.json"
343
+
344
+ data = {}
345
+ for provider, query_types in self.profiles.items():
346
+ data[provider] = {}
347
+ for query_type, profile in query_types.items():
348
+ data[provider][query_type.value] = {
349
+ 'provider_name': profile.provider_name,
350
+ 'query_type': query_type.value,
351
+ 'total_requests': profile.total_requests,
352
+ 'successful_requests': profile.successful_requests,
353
+ 'avg_latency_ms': profile.avg_latency_ms,
354
+ 'p95_latency_ms': profile.p95_latency_ms,
355
+ 'accuracy_score': profile.accuracy_score,
356
+ 'cost_per_request': profile.cost_per_request,
357
+ }
358
+
359
+ with open(profile_file, 'w') as f:
360
+ json.dump(data, f, indent=2)
361
+
362
+ except Exception as e:
363
+ logger.error(f"Failed to save profiles: {e}")
364
+
365
+ def get_status_message(self) -> str:
366
+ """Human-readable status"""
367
+ lines = ["📊 **Provider Selection Status**"]
368
+
369
+ # Provider health
370
+ if self.provider_health:
371
+ lines.append("\n🏥 **Provider Health**")
372
+ for provider, health in sorted(self.provider_health.items()):
373
+ emoji = "🟢" if health > 0.7 else "🟡" if health > 0.3 else "🔴"
374
+ lines.append(f" • {provider}: {emoji} {health:.1%}")
375
+
376
+ # Best providers per query type
377
+ lines.append("\n⭐ **Best Providers by Query Type**")
378
+ for query_type in QueryType:
379
+ rankings = self.get_provider_rankings(query_type)
380
+ if rankings:
381
+ best_provider, score = rankings[0]
382
+ lines.append(f" • {query_type.value}: {best_provider} (score: {score:.1f})")
383
+
384
+ return "\n".join(lines)
385
+
386
+
387
+ # Global instance
388
+ adaptive_selector = AdaptiveProviderSelector()
389
+
390
+
391
+ if __name__ == "__main__":
392
+ # Test the adaptive selector
393
+ selector = AdaptiveProviderSelector()
394
+
395
+ # Simulate some usage
396
+ for i in range(20):
397
+ provider = selector.select_provider(
398
+ QueryType.CODE_GENERATION,
399
+ ["cerebras", "groq", "mistral"]
400
+ )[0]
401
+
402
+ # Simulate result (groq should be slightly better)
403
+ success = (i % 5) != 0 # 80% success
404
+ latency = 100 + (i % 10) * 10
405
+
406
+ selector.record_result(provider, QueryType.CODE_GENERATION, success, latency)
407
+
408
+ print(selector.get_status_message())
409
+ print("\n" + json.dumps(
410
+ selector.get_provider_recommendation(QueryType.CODE_GENERATION, ["cerebras", "groq"]),
411
+ indent=2,
412
+ default=str
413
+ ))