aria-code 4.1.3__py3-none-any.whl
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.
- agents/__init__.py +32 -0
- agents/base.py +190 -0
- agents/deep/__init__.py +37 -0
- agents/deep/calibration_loop.py +144 -0
- agents/deep/critic.py +125 -0
- agents/deep/deepen.py +193 -0
- agents/deep/models.py +149 -0
- agents/deep/pipeline.py +164 -0
- agents/deep/quant_fusion.py +192 -0
- agents/deep/themes.py +95 -0
- agents/deep/tiers.py +106 -0
- agents/financial/__init__.py +10 -0
- agents/financial/catalyst.py +279 -0
- agents/financial/debate.py +145 -0
- agents/financial/earnings.py +303 -0
- agents/financial/fundamental.py +159 -0
- agents/financial/macro.py +99 -0
- agents/financial/news.py +207 -0
- agents/financial/risk.py +132 -0
- agents/financial/sector.py +279 -0
- agents/financial/synthesis.py +274 -0
- agents/financial/technical.py +258 -0
- agents/portfolio_agent.py +333 -0
- agents/realty/__init__.py +62 -0
- agents/realty/asset_diagnosis.py +150 -0
- agents/realty/business_match.py +165 -0
- agents/realty/cashflow_verify.py +208 -0
- agents/realty/contract_rules.py +209 -0
- agents/realty/energy_anomaly.py +188 -0
- agents/realty/exit_settlement.py +207 -0
- agents/realty/fulfillment_risk.py +205 -0
- agents/realty/ops_optimize.py +159 -0
- agents/realty/revenue_share.py +214 -0
- agents/registry.py +144 -0
- agents/sports/__init__.py +0 -0
- agents/sports/football_agent.py +169 -0
- agents/team.py +289 -0
- aliyun_data_client.py +660 -0
- apps/README.md +12 -0
- apps/__init__.py +2 -0
- apps/channels/README.md +15 -0
- apps/cli/README.md +13 -0
- apps/cli/__init__.py +2 -0
- apps/cli/bootstrap.py +99 -0
- apps/cli/codegen_paths.py +29 -0
- apps/cli/commands/__init__.py +16 -0
- apps/cli/commands/analysis_cmds.py +288 -0
- apps/cli/commands/backtest_cmds.py +1887 -0
- apps/cli/commands/broker_cmds.py +1154 -0
- apps/cli/commands/business_workflow_cmds.py +289 -0
- apps/cli/commands/catalog.py +84 -0
- apps/cli/commands/data_cmds.py +405 -0
- apps/cli/commands/diagnostic_cmds.py +179 -0
- apps/cli/commands/diagnostic_ops_cmds.py +696 -0
- apps/cli/commands/finance_render.py +12 -0
- apps/cli/commands/market.py +399 -0
- apps/cli/commands/market_cmds.py +1276 -0
- apps/cli/commands/market_context.py +425 -0
- apps/cli/commands/market_render.py +7 -0
- apps/cli/commands/model_cmds.py +1579 -0
- apps/cli/commands/ops_cmds.py +668 -0
- apps/cli/commands/portfolio_cmds.py +962 -0
- apps/cli/commands/report.py +377 -0
- apps/cli/commands/scaffold_templates.py +617 -0
- apps/cli/commands/session_cmds.py +179 -0
- apps/cli/commands/session_ux_cmds.py +280 -0
- apps/cli/commands/team.py +588 -0
- apps/cli/commands/team_render.py +8 -0
- apps/cli/commands/ui_cmds.py +358 -0
- apps/cli/commands/workflow_cmds.py +279 -0
- apps/cli/commands/workspace_cmds.py +1414 -0
- apps/cli/config_paths.py +70 -0
- apps/cli/config_store.py +61 -0
- apps/cli/deterministic.py +122 -0
- apps/cli/direct.py +48 -0
- apps/cli/github_app_auth.py +135 -0
- apps/cli/handlers/__init__.py +11 -0
- apps/cli/handlers/broker_handlers.py +122 -0
- apps/cli/handlers/chart_handlers.py +1309 -0
- apps/cli/handlers/market_handlers.py +2509 -0
- apps/cli/handlers/realty_handlers.py +114 -0
- apps/cli/handlers/strategy_advice.py +82 -0
- apps/cli/hooks.py +180 -0
- apps/cli/i18n.py +284 -0
- apps/cli/intent.py +136 -0
- apps/cli/intent_router.py +217 -0
- apps/cli/lifecycle_hooks.py +48 -0
- apps/cli/main.py +29 -0
- apps/cli/market_metadata.py +135 -0
- apps/cli/market_universe.py +265 -0
- apps/cli/message_processing.py +257 -0
- apps/cli/plan_mode.py +139 -0
- apps/cli/plotly_html.py +15 -0
- apps/cli/prediction_feedback.py +202 -0
- apps/cli/preflight.py +497 -0
- apps/cli/project_aria.py +60 -0
- apps/cli/prompts/__init__.py +0 -0
- apps/cli/prompts/coding.py +658 -0
- apps/cli/prompts/system_prompts.py +531 -0
- apps/cli/prompts/ui.py +434 -0
- apps/cli/providers/__init__.py +1 -0
- apps/cli/providers/base.py +271 -0
- apps/cli/providers/chat_routing.py +80 -0
- apps/cli/providers/llm/__init__.py +1 -0
- apps/cli/providers/llm/ollama_stream.py +1170 -0
- apps/cli/providers/llm/sse_stream.py +216 -0
- apps/cli/providers/runtime_bridge.py +185 -0
- apps/cli/runtime_consumer.py +489 -0
- apps/cli/session_export.py +87 -0
- apps/cli/session_jsonl.py +207 -0
- apps/cli/session_store.py +112 -0
- apps/cli/todo_tracker.py +190 -0
- apps/cli/tools/__init__.py +40 -0
- apps/cli/tools/context.py +46 -0
- apps/cli/tools/file_tools.py +112 -0
- apps/cli/tools/market_tools.py +549 -0
- apps/cli/tools/notebook_tools.py +111 -0
- apps/cli/tools/system_tools.py +669 -0
- apps/cli/tools/write_tools.py +715 -0
- apps/cli/tradingview_bridge.py +434 -0
- apps/cli/update_check.py +152 -0
- apps/cli/utils/__init__.py +0 -0
- apps/cli/utils/market_detect.py +1578 -0
- apps/daemon/README.md +14 -0
- apps/vscode/README.md +115 -0
- apps/vscode/package.json +70 -0
- aria_cli.py +11636 -0
- aria_code-4.1.3.dist-info/METADATA +952 -0
- aria_code-4.1.3.dist-info/RECORD +284 -0
- aria_code-4.1.3.dist-info/WHEEL +5 -0
- aria_code-4.1.3.dist-info/entry_points.txt +2 -0
- aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
- aria_code-4.1.3.dist-info/top_level.txt +50 -0
- aria_daemon.py +1295 -0
- aria_feishu_bot.py +1359 -0
- aria_relay_client.py +182 -0
- aria_relay_server.py +405 -0
- aria_telegram_bot.py +202 -0
- ariarc.py +328 -0
- artifacts.py +491 -0
- backtest_report.py +472 -0
- brokers/__init__.py +72 -0
- brokers/base.py +207 -0
- brokers/capabilities.py +264 -0
- brokers/cn/__init__.py +10 -0
- brokers/cn/easytrader_broker.py +193 -0
- brokers/cn/futu_broker.py +194 -0
- brokers/cn/longbridge_broker.py +190 -0
- brokers/cn/tiger_broker.py +196 -0
- brokers/cn/xtquant_broker.py +175 -0
- brokers/config.py +364 -0
- brokers/intl/__init__.py +5 -0
- brokers/intl/alpaca_broker.py +183 -0
- brokers/intl/ibkr_broker.py +215 -0
- brokers/intl/webull_broker.py +156 -0
- brokers/paper_broker.py +259 -0
- brokers/planning.py +296 -0
- brokers/registry.py +181 -0
- brokers/trading.py +237 -0
- change_store.py +127 -0
- command_safety.py +19 -0
- computer_use_tools.py +504 -0
- dashboard_generator.py +578 -0
- data_analysis_tools.py +808 -0
- data_cleaner.py +483 -0
- data_service.py +481 -0
- datasources/__init__.py +23 -0
- datasources/base.py +166 -0
- datasources/router.py +221 -0
- datasources/sources/__init__.py +15 -0
- datasources/sources/akshare_source.py +269 -0
- datasources/sources/alpha_vantage_source.py +202 -0
- datasources/sources/edgar_source.py +218 -0
- datasources/sources/finnhub_source.py +197 -0
- datasources/sources/fred_source.py +219 -0
- datasources/sources/tushare_source.py +141 -0
- datasources/sources/web_scraper_source.py +278 -0
- datasources/sources/world_bank_source.py +205 -0
- datasources/sources/yfinance_source.py +152 -0
- demo_player.py +204 -0
- doctor.py +508 -0
- file_analysis_tools.py +734 -0
- finance_formulas.py +389 -0
- football_data_client.py +1670 -0
- intent_classifier.py +358 -0
- local_finance_tools.py +3221 -0
- local_llm_provider.py +552 -0
- macro_tools.py +368 -0
- market_data_client.py +1899 -0
- mcp_client.py +506 -0
- memory_manager.py +245 -0
- model_capability.py +416 -0
- notification_tools.py +248 -0
- packages/__init__.py +23 -0
- packages/aria_agents/__init__.py +5 -0
- packages/aria_agents/manifest.py +69 -0
- packages/aria_core/__init__.py +34 -0
- packages/aria_core/architecture.py +192 -0
- packages/aria_core/export.py +124 -0
- packages/aria_core/manifest.py +65 -0
- packages/aria_infra/__init__.py +15 -0
- packages/aria_infra/arthera.py +52 -0
- packages/aria_infra/doctor.py +246 -0
- packages/aria_infra/product.py +37 -0
- packages/aria_mcp/__init__.py +25 -0
- packages/aria_mcp/bridge.py +38 -0
- packages/aria_mcp/config.py +97 -0
- packages/aria_mcp/tools.py +61 -0
- packages/aria_sdk/__init__.py +19 -0
- packages/aria_sdk/client.py +396 -0
- packages/aria_sdk/providers.py +70 -0
- packages/aria_sdk/streaming.py +73 -0
- packages/aria_sdk/types.py +86 -0
- packages/aria_services/__init__.py +55 -0
- packages/aria_services/context.py +258 -0
- packages/aria_services/data.py +11 -0
- packages/aria_services/provider_health.py +189 -0
- packages/aria_services/registry.py +213 -0
- packages/aria_services/usage.py +138 -0
- packages/aria_skills/__init__.py +5 -0
- packages/aria_skills/registry.py +59 -0
- packages/aria_tools/__init__.py +5 -0
- packages/aria_tools/registry.py +128 -0
- packages/quant_engine/__init__.py +6 -0
- packages/quant_engine/sports/__init__.py +72 -0
- packages/quant_engine/sports/calibrator.py +353 -0
- packages/quant_engine/sports/dixon_coles.py +234 -0
- packages/quant_engine/sports/elo.py +299 -0
- packages/quant_engine/sports/form.py +188 -0
- packages/quant_engine/sports/h2h.py +195 -0
- packages/quant_engine/sports/ml_model.py +354 -0
- packages/quant_engine/sports/predictor.py +311 -0
- packages/quant_engine/sports/tracker.py +664 -0
- packages/quant_engine/stochastic/__init__.py +27 -0
- packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- packages/quant_engine/stochastic/ito_calculus.py +477 -0
- packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- packages/quant_engine/stochastic/options_pricing.py +573 -0
- packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- plan_utils.py +194 -0
- plugin_loader.py +328 -0
- portfolio_ledger.py +262 -0
- privacy/__init__.py +5 -0
- privacy/feedback.py +123 -0
- project_tools.py +525 -0
- providers/__init__.py +30 -0
- providers/llm/__init__.py +19 -0
- providers/llm/anthropic.py +184 -0
- providers/llm/base.py +139 -0
- providers/llm/ollama.py +128 -0
- providers/llm/openai_compat.py +282 -0
- providers/llm/registry.py +358 -0
- realty_data_tools.py +659 -0
- report_generator.py +1314 -0
- runtime/__init__.py +103 -0
- runtime/agent_loop.py +1183 -0
- runtime/approval.py +51 -0
- runtime/events.py +102 -0
- runtime/gateway.py +128 -0
- runtime/lsp.py +346 -0
- runtime/subagent.py +258 -0
- runtime/tool_executor.py +104 -0
- runtime/tool_policy.py +106 -0
- safety/__init__.py +21 -0
- safety/permissions.py +275 -0
- setup_wizard.py +653 -0
- strategy_vault.py +420 -0
- ui/__init__.py +100 -0
- ui/banner.py +310 -0
- ui/completer.py +391 -0
- ui/console.py +271 -0
- ui/image_render.py +243 -0
- ui/input_box.py +376 -0
- ui/picker.py +195 -0
- ui/render/__init__.py +11 -0
- ui/render/finance.py +1480 -0
- ui/render/market.py +225 -0
- ui/render/output.py +681 -0
- ui/render/team.py +346 -0
- ui/robot.py +235 -0
- workspace/__init__.py +6 -0
- workspace/files.py +170 -0
- workspace/verify.py +113 -0
memory_manager.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
memory_manager.py — Aria Code 全局用户 memory 系统
|
|
3
|
+
|
|
4
|
+
存储位置:~/.arthera/memory/
|
|
5
|
+
MEMORY.md ← 索引(每次启动加载)
|
|
6
|
+
user_profile.md ← 用户偏好、交易风格
|
|
7
|
+
project_<slug>.md ← /project load 时自动建档
|
|
8
|
+
research_<topic>.md ← 研究主题(用户触发时创建)
|
|
9
|
+
|
|
10
|
+
公开 API:
|
|
11
|
+
MemoryManager.load_context(max_chars) → 注入 system prompt
|
|
12
|
+
MemoryManager.append(slug, content) → 追加一条事实
|
|
13
|
+
MemoryManager.upsert_project(name, facts) → 项目建档
|
|
14
|
+
MemoryManager.list_all() → 所有条目
|
|
15
|
+
MemoryManager.clear_all() → 清空全局 memory
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
import re
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
_MEMORY_DIR = Path.home() / ".arthera" / "memory"
|
|
30
|
+
_INDEX_FILE = _MEMORY_DIR / "MEMORY.md"
|
|
31
|
+
|
|
32
|
+
_PREF_PATTERNS = [
|
|
33
|
+
(r"(我?不喜欢|我?喜欢|prefer(?:ence)?s?|I always|I never|我总是|我通常)", "preference"),
|
|
34
|
+
(r"(我的风险|风险偏好|risk.*(?:低|高|中|保守|激进)|conservative|aggressive)", "risk_profile"),
|
|
35
|
+
(r"(?:关注|研究|在看|tracking|watching)\s*([A-Z0-9,,、和与&/\s]{2,40})", "watchlist"),
|
|
36
|
+
(r"(我的策略|my strategy|我用|I use)\s+(.{4,40})", "strategy"),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
_SENSITIVE_PATTERN = re.compile(
|
|
40
|
+
r"(\d+[\.,]\d{2,}(?:%|元|USD|HKD|CNY|万|亿)?|持仓|盈亏|亏损|盈利|买入|卖出|成本价)"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _slugify(name: str) -> str:
|
|
45
|
+
name = re.sub(r"[^\w\-]", "_", name.lower())
|
|
46
|
+
return re.sub(r"_+", "_", name).strip("_")[:40]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class MemoryManager:
|
|
50
|
+
def __init__(self, root: Optional[Path] = None):
|
|
51
|
+
self.root = root or _MEMORY_DIR
|
|
52
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
self._index = self.root / "MEMORY.md"
|
|
54
|
+
|
|
55
|
+
# ── Index management ──────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
def _read_index(self) -> list[dict]:
|
|
58
|
+
if not self._index.exists():
|
|
59
|
+
return []
|
|
60
|
+
entries = []
|
|
61
|
+
for line in self._index.read_text(encoding="utf-8").splitlines():
|
|
62
|
+
m = re.match(r"-\s+\[(.+?)\]\((.+?)\)\s*—\s*(.*)", line)
|
|
63
|
+
if m:
|
|
64
|
+
entries.append({"title": m.group(1), "file": m.group(2), "summary": m.group(3)})
|
|
65
|
+
return entries
|
|
66
|
+
|
|
67
|
+
def _write_index(self, entries: list[dict]) -> None:
|
|
68
|
+
lines = ["# Aria Memory Index\n"]
|
|
69
|
+
for e in entries:
|
|
70
|
+
lines.append(f"- [{e['title']}]({e['file']}) — {e['summary']}")
|
|
71
|
+
self._index.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
72
|
+
|
|
73
|
+
def _upsert_index(self, file: str, title: str, summary: str) -> None:
|
|
74
|
+
entries = self._read_index()
|
|
75
|
+
for e in entries:
|
|
76
|
+
if e["file"] == file:
|
|
77
|
+
e["title"] = title
|
|
78
|
+
e["summary"] = summary
|
|
79
|
+
self._write_index(entries)
|
|
80
|
+
return
|
|
81
|
+
entries.append({"title": title, "file": file, "summary": summary})
|
|
82
|
+
self._write_index(entries)
|
|
83
|
+
|
|
84
|
+
# ── Public API ────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
def load_context(self, max_chars: int = 500) -> str:
|
|
87
|
+
"""Return a compact memory block for injection into the system prompt."""
|
|
88
|
+
if not self._index.exists():
|
|
89
|
+
return ""
|
|
90
|
+
snippets = []
|
|
91
|
+
for entry in self._read_index():
|
|
92
|
+
fpath = self.root / entry["file"]
|
|
93
|
+
if not fpath.exists():
|
|
94
|
+
continue
|
|
95
|
+
text = fpath.read_text(encoding="utf-8").strip()
|
|
96
|
+
lines = [l for l in text.splitlines() if l.strip() and not l.startswith("#")]
|
|
97
|
+
snippets.extend(lines[:6])
|
|
98
|
+
|
|
99
|
+
if not snippets:
|
|
100
|
+
return ""
|
|
101
|
+
|
|
102
|
+
block = "\n".join(snippets)
|
|
103
|
+
if len(block) > max_chars:
|
|
104
|
+
block = block[:max_chars] + "…"
|
|
105
|
+
return f"## User Memory\n{block}\n"
|
|
106
|
+
|
|
107
|
+
def append(self, slug: str, content: str, title: Optional[str] = None) -> None:
|
|
108
|
+
"""Append a fact to slug.md, creating the file if needed."""
|
|
109
|
+
if _SENSITIVE_PATTERN.search(content):
|
|
110
|
+
logger.debug("Memory: skipping sensitive content: %s…", content[:40])
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
slug = _slugify(slug)
|
|
114
|
+
fpath = self.root / f"{slug}.md"
|
|
115
|
+
ts = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
116
|
+
|
|
117
|
+
if not fpath.exists():
|
|
118
|
+
_title = title or slug.replace("_", " ").title()
|
|
119
|
+
fpath.write_text(f"# {_title}\n\n", encoding="utf-8")
|
|
120
|
+
self._upsert_index(fpath.name, _title, content[:80])
|
|
121
|
+
|
|
122
|
+
with fpath.open("a", encoding="utf-8") as f:
|
|
123
|
+
f.write(f"- [{ts}] {content}\n")
|
|
124
|
+
|
|
125
|
+
entry = next((e for e in self._read_index() if e["file"] == fpath.name), None)
|
|
126
|
+
if entry:
|
|
127
|
+
entry["summary"] = content[:80]
|
|
128
|
+
self._write_index(self._read_index())
|
|
129
|
+
|
|
130
|
+
logger.debug("Memory: appended to %s: %s", fpath.name, content[:60])
|
|
131
|
+
|
|
132
|
+
def upsert_project(self, name: str, facts: dict) -> None:
|
|
133
|
+
"""Create or refresh a project memory file."""
|
|
134
|
+
slug = f"project_{_slugify(name)}"
|
|
135
|
+
fpath = self.root / f"{slug}.md"
|
|
136
|
+
langs = ", ".join(facts.get("languages", [])[:4]) or "unknown"
|
|
137
|
+
ptype = facts.get("type", "unknown")
|
|
138
|
+
root = facts.get("root", "")
|
|
139
|
+
ts = facts.get("last_loaded", datetime.now().isoformat())[:10]
|
|
140
|
+
syms = ", ".join(facts.get("default_symbols", [])[:5])
|
|
141
|
+
|
|
142
|
+
lines = [
|
|
143
|
+
f"# Project: {name}",
|
|
144
|
+
f"",
|
|
145
|
+
f"- **type**: {ptype}",
|
|
146
|
+
f"- **languages**: {langs}",
|
|
147
|
+
f"- **root**: {root}",
|
|
148
|
+
f"- **last loaded**: {ts}",
|
|
149
|
+
]
|
|
150
|
+
if syms:
|
|
151
|
+
lines.append(f"- **default symbols**: {syms}")
|
|
152
|
+
|
|
153
|
+
fpath.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
154
|
+
summary = f"{ptype} · {langs} · last {ts}"
|
|
155
|
+
self._upsert_index(fpath.name, f"Project: {name}", summary)
|
|
156
|
+
logger.debug("Memory: upserted project %s", name)
|
|
157
|
+
|
|
158
|
+
def list_all(self) -> list[dict]:
|
|
159
|
+
"""Return all memory entries with their file content."""
|
|
160
|
+
result = []
|
|
161
|
+
for entry in self._read_index():
|
|
162
|
+
fpath = self.root / entry["file"]
|
|
163
|
+
content = fpath.read_text(encoding="utf-8").strip() if fpath.exists() else ""
|
|
164
|
+
result.append({**entry, "content": content})
|
|
165
|
+
return result
|
|
166
|
+
|
|
167
|
+
def clear_all(self) -> int:
|
|
168
|
+
"""Delete all memory files and reset the index. Returns count deleted."""
|
|
169
|
+
count = 0
|
|
170
|
+
for fpath in self.root.glob("*.md"):
|
|
171
|
+
if fpath.name != "MEMORY.md":
|
|
172
|
+
fpath.unlink()
|
|
173
|
+
count += 1
|
|
174
|
+
self._index.write_text("# Aria Memory Index\n", encoding="utf-8")
|
|
175
|
+
return count
|
|
176
|
+
|
|
177
|
+
def fact_count(self) -> int:
|
|
178
|
+
return len(self._read_index())
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ── Preference signal extractor (rule-based, zero LLM cost) ──────────────────
|
|
182
|
+
|
|
183
|
+
# Patterns that signal a user revealed an actionable fact mid-conversation
|
|
184
|
+
_MID_CONV_PATTERNS = [
|
|
185
|
+
# Explicit remember requests
|
|
186
|
+
(r"(记住|帮我记|remember that|please remember|note that)\s+(.{5,80})", "user_note"),
|
|
187
|
+
# Risk / style preferences revealed mid-chat
|
|
188
|
+
(r"(我(的)?风险|my risk|风险偏好|risk preference|risk tolerance)[^。.]{0,40}(低|高|中|保守|激进|低风险|高风险)", "risk_profile"),
|
|
189
|
+
(r"(我(喜欢|偏好|倾向|通常用)|I (prefer|like|usually use|always use))\s*(.{4,60})", "preference"),
|
|
190
|
+
# Symbols the user says they're tracking (single or multiple)
|
|
191
|
+
(r"(我(在看|关注|持有|跟踪)|I('m)? (watching|tracking|holding))\s*(.{2,40})", "watchlist"),
|
|
192
|
+
# Explicit remember / 记住 requests
|
|
193
|
+
(r"(记住|帮我记|please remember|note that)\s*(.{4,80})", "user_note"),
|
|
194
|
+
# Stop-loss / take-profit thresholds
|
|
195
|
+
(r"(止损|止盈|stop[- ]loss|take[- ]profit)[^\d]{0,10}(\d+\.?\d*\s*%)", "trading_rule"),
|
|
196
|
+
# Portfolio size hint (non-sensitive: just "大仓位" not actual amounts)
|
|
197
|
+
(r"(大仓|小仓|主仓|重仓|满仓|空仓|half position|full position|light position)", "position_style"),
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
_SENSITIVE_PATTERN_STRICT = re.compile(
|
|
201
|
+
r"(\d[\d,,.]+(?:万|亿|元|USD|CNY|HKD|K|M)?|\b\d{5,}\b|持仓金额|账户余额|本金)"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def extract_preference_signal(user_msg: str, assistant_response: str) -> Optional[str]:
|
|
206
|
+
"""Detect preference/fact signals worth persisting from a user message.
|
|
207
|
+
|
|
208
|
+
Returns a single-line fact string or None. Conservative by design —
|
|
209
|
+
most queries return None. Skips anything containing sensitive amounts.
|
|
210
|
+
"""
|
|
211
|
+
# CJK characters are each 1 codepoint but convey more meaning per char,
|
|
212
|
+
# so use a shorter minimum (8 chars) to avoid filtering valid short acks.
|
|
213
|
+
if len(assistant_response) < 8:
|
|
214
|
+
return None
|
|
215
|
+
if _SENSITIVE_PATTERN.search(user_msg) or _SENSITIVE_PATTERN_STRICT.search(user_msg):
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
for pattern, category in _PREF_PATTERNS + _MID_CONV_PATTERNS:
|
|
219
|
+
m = re.search(pattern, user_msg, re.IGNORECASE)
|
|
220
|
+
if m:
|
|
221
|
+
snippet = user_msg[:120].strip().replace("\n", " ")
|
|
222
|
+
return f"[{category}] {snippet}"
|
|
223
|
+
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def auto_capture_from_turn(
|
|
228
|
+
user_msg: str,
|
|
229
|
+
assistant_response: str,
|
|
230
|
+
memory: "MemoryManager",
|
|
231
|
+
) -> Optional[str]:
|
|
232
|
+
"""Called after each agent turn to auto-persist any detectable user preferences.
|
|
233
|
+
|
|
234
|
+
Returns the captured fact string if something was saved, else None.
|
|
235
|
+
This is intentionally lightweight: it runs synchronously after every turn
|
|
236
|
+
and must not block or throw.
|
|
237
|
+
"""
|
|
238
|
+
try:
|
|
239
|
+
fact = extract_preference_signal(user_msg, assistant_response)
|
|
240
|
+
if fact:
|
|
241
|
+
memory.append("user_preferences", fact, title="用户偏好与设置")
|
|
242
|
+
return fact
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
245
|
+
return None
|
model_capability.py
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""
|
|
2
|
+
model_capability.py — Aria Code model capability registry & tool-call adapter.
|
|
3
|
+
|
|
4
|
+
Responsibilities:
|
|
5
|
+
- Know which local models support native tool calling vs text-based <tool_call>
|
|
6
|
+
- Normalise tool call output across Ollama-native / XML-tag / JSON-fenced formats
|
|
7
|
+
- Inject the correct tool schema format into the Ollama payload
|
|
8
|
+
- Detect capability dynamically when a model is not in the registry
|
|
9
|
+
|
|
10
|
+
Usage::
|
|
11
|
+
|
|
12
|
+
from model_capability import get_model_capability, build_ollama_tool_payload
|
|
13
|
+
|
|
14
|
+
caps = get_model_capability("qwen2.5-coder:7b")
|
|
15
|
+
# {"tool_calls": True, "format": "ollama_native", "context_window": 32768, ...}
|
|
16
|
+
|
|
17
|
+
calls = parse_tool_calls_from_response(full_text, native_tool_calls)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import re
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Capability catalogue
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ModelCapability:
|
|
33
|
+
# Whether the model reliably produces structured tool calls
|
|
34
|
+
tool_calls: bool = False
|
|
35
|
+
# "ollama_native" → Ollama's message.tool_calls list
|
|
36
|
+
# "xml_tags" → <tool_call>{"name":…,"arguments":{…}}</tool_call>
|
|
37
|
+
# "json_fence" → ```json\n{"tool":…,"arguments":{…}}\n```
|
|
38
|
+
# "text_only" → no tool calling; prompt must ask for plain-text answers
|
|
39
|
+
# "router_only" → model is only suitable for intent classification / routing;
|
|
40
|
+
# MUST NOT handle coding, analysis, or multi-step tasks
|
|
41
|
+
format: str = "text_only"
|
|
42
|
+
context_window: int = 8192
|
|
43
|
+
thinking: bool = False # extended-reasoning / <think> tokens
|
|
44
|
+
vision: bool = False # supports image / multimodal input
|
|
45
|
+
finance_tuned: bool = False # model has finance-domain fine-tuning
|
|
46
|
+
# Recommended sampling params
|
|
47
|
+
temperature: float = 0.3
|
|
48
|
+
top_p: float = 0.9
|
|
49
|
+
# Maximum simultaneous tool calls per round (safety limit)
|
|
50
|
+
max_parallel_tools: int = 1
|
|
51
|
+
# Minimum model size class: "nano" <1B / "small" 1-4B / "medium" 4-14B / "large" >14B
|
|
52
|
+
size_class: str = "medium"
|
|
53
|
+
# Extra notes shown in /models list
|
|
54
|
+
notes: str = ""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def is_router_only(cap: "ModelCapability") -> bool:
|
|
58
|
+
"""Return True if this model must NOT handle complex tasks (coding/analysis)."""
|
|
59
|
+
return cap.format == "router_only"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def can_handle_coding(cap: "ModelCapability") -> bool:
|
|
63
|
+
"""Return True if the model is large/capable enough for code generation tasks."""
|
|
64
|
+
return (
|
|
65
|
+
cap.format in ("ollama_native", "xml_tags", "anthropic_native")
|
|
66
|
+
and cap.context_window >= 8192
|
|
67
|
+
and cap.size_class not in ("nano",)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def can_handle_analysis(cap: "ModelCapability") -> bool:
|
|
72
|
+
"""Return True if the model can handle multi-step financial analysis."""
|
|
73
|
+
return (
|
|
74
|
+
cap.format not in ("router_only",)
|
|
75
|
+
and cap.context_window >= 4096
|
|
76
|
+
and cap.size_class not in ("nano",)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Prefix → capability mapping. Longest prefix wins.
|
|
81
|
+
_CAPABILITY_TABLE: Dict[str, ModelCapability] = {
|
|
82
|
+
# ── Qwen family ────────────────────────────────────────────────────────
|
|
83
|
+
"qwen2.5-coder:32b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.2, size_class="large", notes="Best local code+finance model"),
|
|
84
|
+
"qwen2.5-coder:14b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.2, size_class="large"),
|
|
85
|
+
"qwen2.5-coder:7b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.2, size_class="medium"),
|
|
86
|
+
"qwen2.5-coder:3b": ModelCapability(tool_calls=True, format="ollama_native", context_window=32768, temperature=0.3, size_class="small"),
|
|
87
|
+
# 1.5B — too small for reliable tool calls; use text_only to prevent JSON hallucination
|
|
88
|
+
"qwen2.5-coder:1.5b": ModelCapability(tool_calls=False, format="text_only", context_window=8192, temperature=0.4, size_class="small", notes="1.5B — no tools; simple Q&A only"),
|
|
89
|
+
# 0.5B — nano class; only suitable for routing/classification, never complex tasks
|
|
90
|
+
"qwen2.5-coder:0.5b": ModelCapability(tool_calls=False, format="router_only", context_window=4096, temperature=0.5, size_class="nano", notes="0.5B nano — routing/intent only"),
|
|
91
|
+
"qwen2.5-coder": ModelCapability(tool_calls=True, format="ollama_native", context_window=32768, temperature=0.2, size_class="medium"),
|
|
92
|
+
"qwen2.5:72b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.2, notes="Strongest Qwen general model"),
|
|
93
|
+
"qwen2.5:32b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.2),
|
|
94
|
+
"qwen2.5:14b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.2),
|
|
95
|
+
"qwen2.5:7b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3),
|
|
96
|
+
"qwen2.5": ModelCapability(tool_calls=True, format="ollama_native", context_window=32768, temperature=0.3),
|
|
97
|
+
"qwen3": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3, thinking=True),
|
|
98
|
+
"qwq": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3, thinking=True, notes="Math/reasoning focused"),
|
|
99
|
+
# ── DeepSeek family ────────────────────────────────────────────────────
|
|
100
|
+
"deepseek-r1:671b": ModelCapability(tool_calls=False, format="xml_tags", context_window=131072, temperature=0.3, thinking=True, notes="State-of-the-art reasoning"),
|
|
101
|
+
"deepseek-r1:70b": ModelCapability(tool_calls=False, format="xml_tags", context_window=131072, temperature=0.3, thinking=True),
|
|
102
|
+
"deepseek-r1:32b": ModelCapability(tool_calls=False, format="xml_tags", context_window=32768, temperature=0.3, thinking=True),
|
|
103
|
+
"deepseek-r1:14b": ModelCapability(tool_calls=False, format="xml_tags", context_window=32768, temperature=0.3, thinking=True),
|
|
104
|
+
"deepseek-r1:8b": ModelCapability(tool_calls=False, format="xml_tags", context_window=32768, temperature=0.3, thinking=True),
|
|
105
|
+
"deepseek-r1:7b": ModelCapability(tool_calls=False, format="xml_tags", context_window=32768, temperature=0.3, thinking=True),
|
|
106
|
+
"deepseek-r1": ModelCapability(tool_calls=False, format="xml_tags", context_window=32768, temperature=0.3, thinking=True),
|
|
107
|
+
"deepseek-v3.1": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3, notes="DeepSeek V3.1 671B"),
|
|
108
|
+
"deepseek-v3": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3),
|
|
109
|
+
"deepseek-v2.5": ModelCapability(tool_calls=True, format="ollama_native", context_window=65536, temperature=0.3),
|
|
110
|
+
"deepseek-coder-v2": ModelCapability(tool_calls=True, format="ollama_native", context_window=65536, temperature=0.2),
|
|
111
|
+
# ── LLaMA family ───────────────────────────────────────────────────────
|
|
112
|
+
"llama3.3:70b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3, notes="Meta flagship 2024"),
|
|
113
|
+
"llama3.2:90b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3, vision=True, notes="Multimodal, image+text"),
|
|
114
|
+
"llama3.2:11b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3, vision=True, notes="Multimodal, image+text"),
|
|
115
|
+
"llama3.2:3b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3),
|
|
116
|
+
"llama3.2": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3),
|
|
117
|
+
"llama3.1:405b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3),
|
|
118
|
+
"llama3.1:70b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3),
|
|
119
|
+
"llama3.1:8b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3),
|
|
120
|
+
"llama3.1": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3),
|
|
121
|
+
"llama3": ModelCapability(tool_calls=False, format="text_only", context_window=8192, temperature=0.3),
|
|
122
|
+
# ── Mistral family ─────────────────────────────────────────────────────
|
|
123
|
+
"mistral-nemo": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3),
|
|
124
|
+
"mistral-large": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3),
|
|
125
|
+
"mistral-small": ModelCapability(tool_calls=True, format="ollama_native", context_window=32768, temperature=0.3),
|
|
126
|
+
"mistral:7b": ModelCapability(tool_calls=True, format="ollama_native", context_window=32768, temperature=0.3),
|
|
127
|
+
"mistral": ModelCapability(tool_calls=True, format="ollama_native", context_window=32768, temperature=0.3),
|
|
128
|
+
"mixtral": ModelCapability(tool_calls=True, format="ollama_native", context_window=32768, temperature=0.3),
|
|
129
|
+
# ── Phi family ─────────────────────────────────────────────────────────
|
|
130
|
+
"phi4:14b": ModelCapability(tool_calls=True, format="ollama_native", context_window=16384, temperature=0.3, notes="Microsoft Phi4, compact+capable"),
|
|
131
|
+
"phi4": ModelCapability(tool_calls=True, format="ollama_native", context_window=16384, temperature=0.3),
|
|
132
|
+
"phi3.5": ModelCapability(tool_calls=True, format="ollama_native", context_window=16384, temperature=0.3),
|
|
133
|
+
"phi3": ModelCapability(tool_calls=False, format="text_only", context_window=8192, temperature=0.3),
|
|
134
|
+
# ── Google Gemma ───────────────────────────────────────────────────────
|
|
135
|
+
"gemma3:27b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3, vision=True),
|
|
136
|
+
"gemma3:12b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3, vision=True),
|
|
137
|
+
"gemma3:4b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3, vision=True),
|
|
138
|
+
"gemma3": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3, vision=True),
|
|
139
|
+
"gemma2": ModelCapability(tool_calls=False, format="text_only", context_window=8192, temperature=0.3),
|
|
140
|
+
# ── Vision / multimodal models ─────────────────────────────────────────────
|
|
141
|
+
"llava:34b": ModelCapability(tool_calls=False, format="text_only", context_window=4096, temperature=0.3, vision=True, notes="LLaVA 34B vision-language"),
|
|
142
|
+
"llava:13b": ModelCapability(tool_calls=False, format="text_only", context_window=4096, temperature=0.3, vision=True),
|
|
143
|
+
"llava:7b": ModelCapability(tool_calls=False, format="text_only", context_window=4096, temperature=0.3, vision=True),
|
|
144
|
+
"llava": ModelCapability(tool_calls=False, format="text_only", context_window=4096, temperature=0.3, vision=True),
|
|
145
|
+
"bakllava": ModelCapability(tool_calls=False, format="text_only", context_window=4096, temperature=0.3, vision=True),
|
|
146
|
+
"moondream": ModelCapability(tool_calls=False, format="text_only", context_window=2048, temperature=0.3, vision=True, size_class="small", notes="Tiny vision model"),
|
|
147
|
+
"minicpm-v": ModelCapability(tool_calls=False, format="text_only", context_window=8192, temperature=0.3, vision=True, size_class="small"),
|
|
148
|
+
"qwen2-vl:72b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3, vision=True, notes="Qwen2-VL 72B multimodal"),
|
|
149
|
+
"qwen2-vl:7b": ModelCapability(tool_calls=True, format="ollama_native", context_window=32768, temperature=0.3, vision=True),
|
|
150
|
+
"qwen2-vl": ModelCapability(tool_calls=True, format="ollama_native", context_window=32768, temperature=0.3, vision=True),
|
|
151
|
+
"qwen2.5vl:72b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3, vision=True, notes="Qwen2.5-VL 72B"),
|
|
152
|
+
"qwen2.5vl:7b": ModelCapability(tool_calls=True, format="ollama_native", context_window=32768, temperature=0.3, vision=True),
|
|
153
|
+
"qwen2.5vl": ModelCapability(tool_calls=True, format="ollama_native", context_window=32768, temperature=0.3, vision=True),
|
|
154
|
+
# ── Finance-specific ───────────────────────────────────────────────────
|
|
155
|
+
"finma": ModelCapability(tool_calls=False, format="text_only", context_window=4096, temperature=0.3, finance_tuned=True),
|
|
156
|
+
"fingpt": ModelCapability(tool_calls=False, format="text_only", context_window=4096, temperature=0.3, finance_tuned=True),
|
|
157
|
+
"bloomberggpt": ModelCapability(tool_calls=False, format="text_only", context_window=2048, temperature=0.2, finance_tuned=True),
|
|
158
|
+
# ── Aria own models ────────────────────────────────────────────────────
|
|
159
|
+
# aria-sonata-3b: primary local production model — 3B, good for finance Q&A + tool calls
|
|
160
|
+
"aria-sonata-3b": ModelCapability(tool_calls=False, format="xml_tags", context_window=16384, temperature=0.3, size_class="small", finance_tuned=True, notes="3B local production model"),
|
|
161
|
+
# aria-sonata 1.x/0.5B series — GGUF Ollama models; xml_tags for text-based tool parsing
|
|
162
|
+
"aria-sonata:4.5-thinking": ModelCapability(tool_calls=False, format="xml_tags", context_window=8192, temperature=0.3, size_class="small", thinking=True, finance_tuned=True),
|
|
163
|
+
"aria-sonata:4.5-verbose": ModelCapability(tool_calls=False, format="xml_tags", context_window=8192, temperature=0.3, size_class="small", finance_tuned=True),
|
|
164
|
+
"aria-sonata:4.6-thinking": ModelCapability(tool_calls=False, format="xml_tags", context_window=8192, temperature=0.3, size_class="small", thinking=True, finance_tuned=True),
|
|
165
|
+
"aria-sonata": ModelCapability(tool_calls=False, format="xml_tags", context_window=8192, temperature=0.3, size_class="small", finance_tuned=True),
|
|
166
|
+
# aria-prelude: nano router model — ONLY for intent classification and routing
|
|
167
|
+
"aria-prelude": ModelCapability(tool_calls=False, format="router_only", context_window=4096, temperature=0.2, size_class="nano", finance_tuned=True, notes="Nano router — intent classification only"),
|
|
168
|
+
# ── Anthropic Claude (cloud API via providers/llm/anthropic.py) ───────
|
|
169
|
+
# format="anthropic_native" → tool calling via Anthropic SDK, not Ollama
|
|
170
|
+
# All Claude 3+ models share 200K context and native vision support.
|
|
171
|
+
"claude-opus-4-8": ModelCapability(tool_calls=True, format="anthropic_native", context_window=200000, temperature=0.3, size_class="large", vision=True, thinking=True, notes="Claude Opus 4.8 — most capable"),
|
|
172
|
+
"claude-opus-4": ModelCapability(tool_calls=True, format="anthropic_native", context_window=200000, temperature=0.3, size_class="large", vision=True, thinking=True),
|
|
173
|
+
"claude-sonnet-4-6": ModelCapability(tool_calls=True, format="anthropic_native", context_window=200000, temperature=0.3, size_class="large", vision=True),
|
|
174
|
+
"claude-sonnet-4": ModelCapability(tool_calls=True, format="anthropic_native", context_window=200000, temperature=0.3, size_class="large", vision=True),
|
|
175
|
+
"claude-haiku-4-5": ModelCapability(tool_calls=True, format="anthropic_native", context_window=200000, temperature=0.3, size_class="medium", vision=True, notes="Claude Haiku 4.5 — fast/cheap"),
|
|
176
|
+
"claude-haiku-4": ModelCapability(tool_calls=True, format="anthropic_native", context_window=200000, temperature=0.3, size_class="medium", vision=True),
|
|
177
|
+
# Claude 3.x legacy — still widely used
|
|
178
|
+
"claude-3-7-sonnet": ModelCapability(tool_calls=True, format="anthropic_native", context_window=200000, temperature=0.3, size_class="large", vision=True, thinking=True),
|
|
179
|
+
"claude-3-5-sonnet": ModelCapability(tool_calls=True, format="anthropic_native", context_window=200000, temperature=0.3, size_class="large", vision=True),
|
|
180
|
+
"claude-3-5-haiku": ModelCapability(tool_calls=True, format="anthropic_native", context_window=200000, temperature=0.3, size_class="medium", vision=True),
|
|
181
|
+
"claude-3-opus": ModelCapability(tool_calls=True, format="anthropic_native", context_window=200000, temperature=0.3, size_class="large", vision=True, thinking=True),
|
|
182
|
+
"claude-3-sonnet": ModelCapability(tool_calls=True, format="anthropic_native", context_window=200000, temperature=0.3, size_class="large", vision=True),
|
|
183
|
+
"claude-3-haiku": ModelCapability(tool_calls=True, format="anthropic_native", context_window=200000, temperature=0.3, size_class="medium", vision=True),
|
|
184
|
+
# Generic prefix catch-all for future Claude versions (longest-prefix matching ensures
|
|
185
|
+
# specific entries above still win over this fallback)
|
|
186
|
+
"claude": ModelCapability(tool_calls=True, format="anthropic_native", context_window=200000, temperature=0.3, size_class="large", vision=True),
|
|
187
|
+
# ── Arthera cloud-routed models (large, run via cloud API) ────────────
|
|
188
|
+
"gpt-oss:120b": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3, vision=True, notes="GPT-OSS 120B cloud"),
|
|
189
|
+
"gpt-oss": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3, vision=True, notes="GPT-OSS cloud"),
|
|
190
|
+
"deepseek-v3.1:671b-cloud": ModelCapability(tool_calls=True, format="ollama_native", context_window=131072, temperature=0.3, notes="DeepSeek V3.1 671B cloud"),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Default fallback when model is unknown
|
|
194
|
+
_DEFAULT_CAPABILITY = ModelCapability(
|
|
195
|
+
tool_calls=False, format="text_only", context_window=4096, temperature=0.5,
|
|
196
|
+
notes="Unknown model — conservative settings applied",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_model_capability(model_name: str) -> ModelCapability:
|
|
201
|
+
"""
|
|
202
|
+
Return capability for *model_name* using longest-prefix matching.
|
|
203
|
+
|
|
204
|
+
Examples::
|
|
205
|
+
|
|
206
|
+
get_model_capability("qwen2.5-coder:7b-instruct-q4_K_M")
|
|
207
|
+
# → same as "qwen2.5-coder:7b" entry
|
|
208
|
+
"""
|
|
209
|
+
name = (model_name or "").strip().lower()
|
|
210
|
+
# Strip GGUF quantisation suffixes like :q4_k_m, :f16, etc.
|
|
211
|
+
clean = re.sub(r":[qfQ][0-9].*$", "", name)
|
|
212
|
+
# Also strip common instruct/chat/gguf tags appended after the size
|
|
213
|
+
clean = re.sub(r"-(instruct|chat|gguf|base|it)$", "", clean)
|
|
214
|
+
|
|
215
|
+
best_prefix = ""
|
|
216
|
+
best_cap = _DEFAULT_CAPABILITY
|
|
217
|
+
for prefix, cap in _CAPABILITY_TABLE.items():
|
|
218
|
+
if clean.startswith(prefix.lower()) and len(prefix) > len(best_prefix):
|
|
219
|
+
best_prefix = prefix
|
|
220
|
+
best_cap = cap
|
|
221
|
+
|
|
222
|
+
return best_cap
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
# Tool schema injection helpers
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
def build_ollama_tool_payload(
|
|
230
|
+
tools_schema: List[Dict],
|
|
231
|
+
model_name: str,
|
|
232
|
+
) -> Optional[List[Dict]]:
|
|
233
|
+
"""
|
|
234
|
+
Return the `tools` field for an Ollama /api/chat request, or None when the
|
|
235
|
+
model does not support native tool calling.
|
|
236
|
+
|
|
237
|
+
When format == "ollama_native" the schema is passed as-is.
|
|
238
|
+
When format == "xml_tags" we skip the field and rely on prompt injection.
|
|
239
|
+
"""
|
|
240
|
+
cap = get_model_capability(model_name)
|
|
241
|
+
if not cap.tool_calls or cap.format != "ollama_native":
|
|
242
|
+
return None
|
|
243
|
+
return tools_schema
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def build_tool_system_prompt(
|
|
247
|
+
tools_schema: List[Dict],
|
|
248
|
+
model_name: str,
|
|
249
|
+
) -> str:
|
|
250
|
+
"""
|
|
251
|
+
For models that do NOT support native tool calls (xml_tags / text_only),
|
|
252
|
+
return a system-prompt block that instructs the model to emit
|
|
253
|
+
``<tool_call>{"name":…,"arguments":{…}}</tool_call>`` tags.
|
|
254
|
+
"""
|
|
255
|
+
cap = get_model_capability(model_name)
|
|
256
|
+
if cap.tool_calls and cap.format == "ollama_native":
|
|
257
|
+
return "" # handled by native API
|
|
258
|
+
|
|
259
|
+
tool_list = []
|
|
260
|
+
for t in tools_schema:
|
|
261
|
+
fn = t.get("function", t)
|
|
262
|
+
params = fn.get("parameters", {}).get("properties", {})
|
|
263
|
+
required = fn.get("parameters", {}).get("required", [])
|
|
264
|
+
param_str = ", ".join(
|
|
265
|
+
f"{k}: {v.get('type','any')}{'*' if k in required else ''}"
|
|
266
|
+
for k, v in params.items()
|
|
267
|
+
)
|
|
268
|
+
tool_list.append(f" - {fn['name']}({param_str}): {fn.get('description','')}")
|
|
269
|
+
|
|
270
|
+
tools_block = "\n".join(tool_list)
|
|
271
|
+
return (
|
|
272
|
+
"\n\n## Available Tools\n\n"
|
|
273
|
+
"When you need to call a tool, output EXACTLY this format (nothing else on the line):\n\n"
|
|
274
|
+
'<tool_call>{"name": "tool_name", "arguments": {"param": "value"}}</tool_call>\n\n'
|
|
275
|
+
"Tools available:\n"
|
|
276
|
+
f"{tools_block}\n\n"
|
|
277
|
+
"Rules:\n"
|
|
278
|
+
"1. Call ONE tool at a time. Wait for the result before calling the next.\n"
|
|
279
|
+
"2. After receiving a tool result, continue your analysis or call another tool.\n"
|
|
280
|
+
"3. When done with all tools, write your final answer in plain text.\n"
|
|
281
|
+
"4. Never make up tool results — always call the tool.\n"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
# Tool call parsers
|
|
287
|
+
# ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
def parse_tool_calls_from_response(
|
|
290
|
+
text: str,
|
|
291
|
+
native_calls: Optional[List[Dict]] = None,
|
|
292
|
+
model_name: str = "",
|
|
293
|
+
) -> List[Dict[str, Any]]:
|
|
294
|
+
"""
|
|
295
|
+
Unified parser. Returns list of {"tool": str, "params": dict}.
|
|
296
|
+
|
|
297
|
+
Priority:
|
|
298
|
+
1. native_calls (Ollama tool_calls list) — most reliable
|
|
299
|
+
2. XML tags <tool_call>…</tool_call>
|
|
300
|
+
3. JSON code fences ```json … ```
|
|
301
|
+
4. Raw JSON object containing "name"/"arguments" keys
|
|
302
|
+
"""
|
|
303
|
+
# 1. Native Ollama tool calls
|
|
304
|
+
if native_calls:
|
|
305
|
+
result = []
|
|
306
|
+
for tc in native_calls:
|
|
307
|
+
fn = tc.get("function", tc)
|
|
308
|
+
name = fn.get("name", "")
|
|
309
|
+
args = fn.get("arguments", {})
|
|
310
|
+
if isinstance(args, str):
|
|
311
|
+
try:
|
|
312
|
+
args = json.loads(args)
|
|
313
|
+
except json.JSONDecodeError:
|
|
314
|
+
args = {}
|
|
315
|
+
if name:
|
|
316
|
+
result.append({"tool": name, "params": args})
|
|
317
|
+
if result:
|
|
318
|
+
return result
|
|
319
|
+
|
|
320
|
+
if not text:
|
|
321
|
+
return []
|
|
322
|
+
|
|
323
|
+
results: List[Dict[str, Any]] = []
|
|
324
|
+
|
|
325
|
+
# 2. XML tag format: <tool_call>…</tool_call>
|
|
326
|
+
xml_pattern = re.compile(
|
|
327
|
+
r"<tool_call>\s*(.*?)\s*</tool_call>", re.DOTALL | re.IGNORECASE
|
|
328
|
+
)
|
|
329
|
+
for m in xml_pattern.finditer(text):
|
|
330
|
+
tc = _try_parse_json(m.group(1))
|
|
331
|
+
if tc:
|
|
332
|
+
results.append(_normalise_call(tc))
|
|
333
|
+
|
|
334
|
+
if results:
|
|
335
|
+
return results
|
|
336
|
+
|
|
337
|
+
# 3. JSON fence: ```json … ``` or ``` … ```
|
|
338
|
+
fence_pattern = re.compile(
|
|
339
|
+
r"```(?:json)?\s*(\{.*?\})\s*```", re.DOTALL | re.IGNORECASE
|
|
340
|
+
)
|
|
341
|
+
for m in fence_pattern.finditer(text):
|
|
342
|
+
tc = _try_parse_json(m.group(1))
|
|
343
|
+
if tc and ("name" in tc or "tool" in tc):
|
|
344
|
+
results.append(_normalise_call(tc))
|
|
345
|
+
|
|
346
|
+
if results:
|
|
347
|
+
return results
|
|
348
|
+
|
|
349
|
+
# 4. Bare JSON object anywhere in text
|
|
350
|
+
json_pattern = re.compile(r"\{[^{}]*\"(?:name|tool)\"[^{}]*\}", re.DOTALL)
|
|
351
|
+
for m in json_pattern.finditer(text):
|
|
352
|
+
tc = _try_parse_json(m.group(0))
|
|
353
|
+
if tc and ("name" in tc or "tool" in tc):
|
|
354
|
+
results.append(_normalise_call(tc))
|
|
355
|
+
|
|
356
|
+
return results
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _try_parse_json(s: str) -> Optional[Dict]:
|
|
360
|
+
try:
|
|
361
|
+
return json.loads(s.strip())
|
|
362
|
+
except (json.JSONDecodeError, TypeError):
|
|
363
|
+
return None
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _normalise_call(tc: Dict) -> Dict[str, Any]:
|
|
367
|
+
"""Normalise various key naming conventions → {"tool": …, "params": …}."""
|
|
368
|
+
name = tc.get("name") or tc.get("tool") or tc.get("function", {}).get("name", "")
|
|
369
|
+
args = (
|
|
370
|
+
tc.get("arguments")
|
|
371
|
+
or tc.get("params")
|
|
372
|
+
or tc.get("parameters")
|
|
373
|
+
or tc.get("function", {}).get("arguments", {})
|
|
374
|
+
or {}
|
|
375
|
+
)
|
|
376
|
+
if isinstance(args, str):
|
|
377
|
+
args = _try_parse_json(args) or {}
|
|
378
|
+
return {"tool": name, "params": args}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# ---------------------------------------------------------------------------
|
|
382
|
+
# Recommended local models for finance work
|
|
383
|
+
# ---------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
RECOMMENDED_FINANCE_MODELS: List[Dict[str, str]] = [
|
|
386
|
+
{
|
|
387
|
+
"model": "qwen2.5-coder:7b",
|
|
388
|
+
"reason": "Best balance of tool calling + code generation for finance scripts",
|
|
389
|
+
"install": "ollama pull qwen2.5-coder:7b",
|
|
390
|
+
"vram_gb": "5",
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"model": "qwen2.5:14b",
|
|
394
|
+
"reason": "Strong quantitative reasoning, multi-turn strategy analysis",
|
|
395
|
+
"install": "ollama pull qwen2.5:14b",
|
|
396
|
+
"vram_gb": "10",
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
"model": "deepseek-r1:14b",
|
|
400
|
+
"reason": "Deep reasoning for complex factor models (slow but thorough)",
|
|
401
|
+
"install": "ollama pull deepseek-r1:14b",
|
|
402
|
+
"vram_gb": "10",
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
"model": "llama3.2:3b",
|
|
406
|
+
"reason": "Ultra-fast for quick quotes and simple questions",
|
|
407
|
+
"install": "ollama pull llama3.2:3b",
|
|
408
|
+
"vram_gb": "2",
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
"model": "phi4:14b",
|
|
412
|
+
"reason": "Math-strong, good for Greeks / derivative pricing",
|
|
413
|
+
"install": "ollama pull phi4",
|
|
414
|
+
"vram_gb": "9",
|
|
415
|
+
},
|
|
416
|
+
]
|