cite-agent 1.3.3__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.
- {cite_agent-1.3.3/cite_agent.egg-info → cite_agent-1.4.3}/PKG-INFO +15 -1
- {cite_agent-1.3.3 → cite_agent-1.4.3}/README.md +15 -1
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/__init__.py +13 -13
- cite_agent-1.4.3/cite_agent/__version__.py +1 -0
- cite_agent-1.4.3/cite_agent/action_first_mode.py +150 -0
- cite_agent-1.4.3/cite_agent/adaptive_providers.py +413 -0
- cite_agent-1.4.3/cite_agent/archive_api_client.py +186 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/auth.py +0 -1
- cite_agent-1.4.3/cite_agent/auto_expander.py +70 -0
- cite_agent-1.4.3/cite_agent/cache.py +379 -0
- cite_agent-1.4.3/cite_agent/circuit_breaker.py +370 -0
- cite_agent-1.4.3/cite_agent/citation_network.py +377 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/cli.py +198 -13
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/cli_conversational.py +113 -3
- cite_agent-1.4.3/cite_agent/confidence_calibration.py +381 -0
- cite_agent-1.4.3/cite_agent/conversation_archive.py +152 -0
- cite_agent-1.4.3/cite_agent/deduplication.py +325 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/enhanced_ai_agent.py +2440 -622
- cite_agent-1.4.3/cite_agent/error_handler.py +228 -0
- cite_agent-1.4.3/cite_agent/execution_safety.py +329 -0
- cite_agent-1.4.3/cite_agent/full_paper_reader.py +239 -0
- cite_agent-1.4.3/cite_agent/observability.py +398 -0
- cite_agent-1.4.3/cite_agent/offline_mode.py +348 -0
- cite_agent-1.4.3/cite_agent/paper_comparator.py +368 -0
- cite_agent-1.4.3/cite_agent/paper_summarizer.py +420 -0
- cite_agent-1.4.3/cite_agent/pdf_extractor.py +350 -0
- cite_agent-1.4.3/cite_agent/proactive_boundaries.py +266 -0
- cite_agent-1.4.3/cite_agent/project_detector.py +148 -0
- cite_agent-1.4.3/cite_agent/quality_gate.py +442 -0
- cite_agent-1.4.3/cite_agent/request_queue.py +390 -0
- cite_agent-1.4.3/cite_agent/response_enhancer.py +257 -0
- cite_agent-1.4.3/cite_agent/response_formatter.py +458 -0
- cite_agent-1.4.3/cite_agent/response_pipeline.py +295 -0
- cite_agent-1.4.3/cite_agent/response_style_enhancer.py +259 -0
- cite_agent-1.4.3/cite_agent/self_healing.py +418 -0
- cite_agent-1.4.3/cite_agent/similarity_finder.py +524 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/streaming_ui.py +13 -9
- cite_agent-1.4.3/cite_agent/thinking_blocks.py +308 -0
- cite_agent-1.4.3/cite_agent/tool_orchestrator.py +416 -0
- cite_agent-1.4.3/cite_agent/trend_analyzer.py +540 -0
- cite_agent-1.4.3/cite_agent/unpaywall_client.py +226 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3/cite_agent.egg-info}/PKG-INFO +15 -1
- cite_agent-1.4.3/cite_agent.egg-info/SOURCES.txt +120 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent.egg-info/top_level.txt +0 -1
- cite_agent-1.4.3/docs/AGENT_INTELLIGENCE_REPORT.md +159 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/docs/BETA_LAUNCH_CHECKLIST.md +2 -0
- cite_agent-1.4.3/docs/BETA_PITCH_v1.3.9.md +95 -0
- cite_agent-1.4.3/docs/BETA_SHOWCASE_GUIDE.md +43 -0
- cite_agent-1.4.3/docs/CONSISTENCY_TEST_RESULTS.md +426 -0
- cite_agent-1.4.3/docs/CONVERSATIONAL_DEPTH_REPORT.md +237 -0
- cite_agent-1.4.3/docs/DEV_NOTES_2025-10-30.md +8 -0
- cite_agent-1.4.3/docs/FINAL_TEST_RESULTS_CEREBRAS.md +416 -0
- cite_agent-1.4.3/docs/HONEST_TESTING_ASSESSMENT.md +339 -0
- cite_agent-1.4.3/docs/MAXIMUM_CAPABILITY_ACHIEVED.md +308 -0
- cite_agent-1.4.3/docs/TEST_RESULTS_2025-11-01.md +161 -0
- cite_agent-1.4.3/docs/TEST_RESULTS_SESSION_2025-11-04.md +195 -0
- cite_agent-1.4.3/docs/TEST_RUN_2025-11-01.md +259 -0
- cite_agent-1.4.3/docs/TEST_RUN_ANALYSIS_CEREBRAS.md +411 -0
- cite_agent-1.4.3/docs/TEST_RUN_UPDATE_2025-11-01.md +217 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/setup.py +1 -1
- cite_agent-1.4.3/tests/enhanced/conftest.py +11 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/tests/enhanced/test_account_client.py +6 -5
- {cite_agent-1.3.3 → cite_agent-1.4.3}/tests/enhanced/test_archive_agent.py +30 -16
- cite_agent-1.4.3/tests/enhanced/test_autonomy_harness.py +124 -0
- cite_agent-1.4.3/tests/enhanced/test_conversation_archive.py +41 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/tests/enhanced/test_enhanced_agent_runtime.py +15 -64
- cite_agent-1.4.3/tests/enhanced/test_financial_planner.py +59 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/tests/enhanced/test_setup_config.py +1 -5
- {cite_agent-1.3.3 → cite_agent-1.4.3}/tests/integration_test.py +12 -12
- cite_agent-1.4.3/tests/test_action_first_mode.py +129 -0
- cite_agent-1.4.3/tests/test_end_to_end.py +165 -0
- cite_agent-1.4.3/tests/test_new_features.py +410 -0
- cite_agent-1.4.3/tests/test_proactive_boundaries.py +201 -0
- cite_agent-1.4.3/tests/test_real_world_scenarios.py +422 -0
- cite_agent-1.4.3/tests/test_robustness_comprehensive.py +339 -0
- cite_agent-1.4.3/tests/test_style_enhancement.py +113 -0
- cite_agent-1.4.3/tests/test_style_with_mock.py +157 -0
- cite_agent-1.3.3/cite_agent/__version__.py +0 -1
- cite_agent-1.3.3/cite_agent.egg-info/SOURCES.txt +0 -91
- cite_agent-1.3.3/src/__init__.py +0 -1
- cite_agent-1.3.3/src/services/__init__.py +0 -132
- cite_agent-1.3.3/src/services/auth_service/__init__.py +0 -3
- cite_agent-1.3.3/src/services/auth_service/auth_manager.py +0 -33
- cite_agent-1.3.3/src/services/graph/__init__.py +0 -1
- cite_agent-1.3.3/src/services/graph/knowledge_graph.py +0 -194
- cite_agent-1.3.3/src/services/llm_service/__init__.py +0 -5
- cite_agent-1.3.3/src/services/llm_service/llm_manager.py +0 -495
- cite_agent-1.3.3/src/services/paper_service/__init__.py +0 -5
- cite_agent-1.3.3/src/services/paper_service/openalex.py +0 -231
- cite_agent-1.3.3/src/services/performance_service/__init__.py +0 -1
- cite_agent-1.3.3/src/services/performance_service/rust_performance.py +0 -395
- cite_agent-1.3.3/src/services/research_service/__init__.py +0 -23
- cite_agent-1.3.3/src/services/research_service/chatbot.py +0 -2056
- cite_agent-1.3.3/src/services/research_service/citation_manager.py +0 -436
- cite_agent-1.3.3/src/services/research_service/context_manager.py +0 -1441
- cite_agent-1.3.3/src/services/research_service/conversation_manager.py +0 -597
- cite_agent-1.3.3/src/services/research_service/critical_paper_detector.py +0 -577
- cite_agent-1.3.3/src/services/research_service/enhanced_research.py +0 -121
- cite_agent-1.3.3/src/services/research_service/enhanced_synthesizer.py +0 -375
- cite_agent-1.3.3/src/services/research_service/query_generator.py +0 -777
- cite_agent-1.3.3/src/services/research_service/synthesizer.py +0 -1273
- cite_agent-1.3.3/src/services/search_service/__init__.py +0 -5
- cite_agent-1.3.3/src/services/search_service/indexer.py +0 -186
- cite_agent-1.3.3/src/services/search_service/search_engine.py +0 -342
- cite_agent-1.3.3/src/services/simple_enhanced_main.py +0 -287
- cite_agent-1.3.3/tests/beta_launch_test_suite.py +0 -369
- cite_agent-1.3.3/tests/test_truth_seeking_comprehensive.py +0 -399
- cite_agent-1.3.3/tools/security_audit.py +0 -149
- {cite_agent-1.3.3 → cite_agent-1.4.3}/LICENSE +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/MANIFEST.in +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/__main__.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/account_client.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/agent_backend_only.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/ascii_plotting.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/backend_only_client.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/cli_enhanced.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/cli_workflow.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/dashboard.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/rate_limiter.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/session_manager.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/setup_config.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/telemetry.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/ui.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/updater.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/web_search.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/workflow.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent/workflow_integration.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent.egg-info/dependency_links.txt +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent.egg-info/entry_points.txt +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/cite_agent.egg-info/requires.txt +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/docs/BETA_RELEASE_CHECKLIST.md +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/docs/ENHANCED_CAPABILITIES.md +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/docs/GROQ_RATE_LIMITS.md +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/docs/INSTALL.md +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/docs/PUBLISHING_PYPI.md +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/docs/SECURE_PACKAGING_GUIDE.md +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/docs/SECURITY_AUDIT.md +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/docs/USER_GETTING_STARTED.md +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/docs/playbooks/BETA_LAUNCH_PLAYBOOK.md +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/requirements.txt +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/setup.cfg +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/tests/enhanced/test_reasoning_engine.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/tests/enhanced/test_tool_framework.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/tests/validation/test_accuracy_system.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/tests/validation/test_agent_live.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/tests/validation/test_backend_local.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/tests/validation/test_cerebras_comparison.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/tests/validation/test_improved_prompt.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/tests/validation/test_qualitative_robustness.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/tests/validation/test_qualitative_system.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/tests/validation/test_truth_seeking_chinese.py +0 -0
- {cite_agent-1.3.3 → cite_agent-1.4.3}/tests/validation/test_truth_seeking_real.py +0 -0
- /cite_agent-1.3.3/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
|
+
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__ = "
|
|
11
|
-
__author__ = "
|
|
12
|
-
__email__ = "contact@
|
|
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 = "
|
|
21
|
+
PACKAGE_NAME = "cite-agent"
|
|
22
22
|
PACKAGE_VERSION = __version__
|
|
23
|
-
PACKAGE_DESCRIPTION = "
|
|
24
|
-
PACKAGE_URL = "https://github.com/Spectating101/
|
|
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
|
-
🚀
|
|
34
|
-
|
|
33
|
+
🚀 Cite Agent Quick Start
|
|
34
|
+
=========================
|
|
35
35
|
|
|
36
36
|
1. Install the package and CLI:
|
|
37
|
-
pip install
|
|
37
|
+
pip install cite-agent
|
|
38
38
|
|
|
39
|
-
2. Configure your
|
|
40
|
-
|
|
39
|
+
2. Configure your account or local keys:
|
|
40
|
+
cite-agent --setup
|
|
41
41
|
|
|
42
42
|
3. Ask a question:
|
|
43
|
-
|
|
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
|
|
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
|
+
))
|