dojoagents 0.1.0__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.
- dojoagents/__init__.py +13 -0
- dojoagents/agent/__init__.py +1 -0
- dojoagents/agent/canvas_protocol.py +46 -0
- dojoagents/agent/compressor.py +377 -0
- dojoagents/agent/context_length.py +27 -0
- dojoagents/agent/dashboard_tool_protocol.py +117 -0
- dojoagents/agent/events.py +258 -0
- dojoagents/agent/gemini_provider.py +673 -0
- dojoagents/agent/guardrails.py +257 -0
- dojoagents/agent/harness.py +149 -0
- dojoagents/agent/harnesses/__init__.py +3 -0
- dojoagents/agent/harnesses/portfolio.py +302 -0
- dojoagents/agent/harnesses/portfolio_eval.py +203 -0
- dojoagents/agent/hooks/__init__.py +0 -0
- dojoagents/agent/hooks/token_compression.py +145 -0
- dojoagents/agent/loop.py +1281 -0
- dojoagents/agent/model_context.py +395 -0
- dojoagents/agent/models.py +181 -0
- dojoagents/agent/multimodal.py +230 -0
- dojoagents/agent/presenters.py +98 -0
- dojoagents/agent/provider_state.py +109 -0
- dojoagents/agent/providers.py +388 -0
- dojoagents/agent/redact.py +51 -0
- dojoagents/agent/runtime.py +273 -0
- dojoagents/agent/session_manager.py +462 -0
- dojoagents/agent/session_models.py +67 -0
- dojoagents/agent/session_repository.py +209 -0
- dojoagents/agent/think_scrubber.py +221 -0
- dojoagents/agent/token_ledger.py +113 -0
- dojoagents/agent/token_policy.py +24 -0
- dojoagents/agent/turn_intent.py +31 -0
- dojoagents/cli/__init__.py +1 -0
- dojoagents/cli/gateway_setup.py +407 -0
- dojoagents/cli/main.py +180 -0
- dojoagents/cli/mcp_serve.py +36 -0
- dojoagents/cli/model_setup.py +147 -0
- dojoagents/cli/precompute_sector.py +90 -0
- dojoagents/config/__init__.py +1 -0
- dojoagents/config/loader.py +333 -0
- dojoagents/config/models.py +206 -0
- dojoagents/config/watcher.py +21 -0
- dojoagents/cron/__init__.py +1 -0
- dojoagents/cron/jobs.py +131 -0
- dojoagents/cron/scheduler.py +31 -0
- dojoagents/dashboard/__init__.py +1 -0
- dojoagents/dashboard/agent_runs.py +207 -0
- dojoagents/dashboard/api.py +5 -0
- dojoagents/dashboard/deps.py +124 -0
- dojoagents/dashboard/frontend_builder.py +87 -0
- dojoagents/dashboard/middleware/__init__.py +1 -0
- dojoagents/dashboard/middleware/profiler_middleware.py +64 -0
- dojoagents/dashboard/routers/__init__.py +27 -0
- dojoagents/dashboard/routers/chat_sessions.py +75 -0
- dojoagents/dashboard/routers/dojo_core.py +238 -0
- dojoagents/dashboard/routers/dojo_folio.py +120 -0
- dojoagents/dashboard/routers/dojo_mesh.py +53 -0
- dojoagents/dashboard/routers/dojo_sphere.py +200 -0
- dojoagents/dashboard/routers/market.py +107 -0
- dojoagents/dashboard/routers/markets.py +29 -0
- dojoagents/dashboard/routers/portfolio.py +262 -0
- dojoagents/dashboard/routers/sector.py +71 -0
- dojoagents/dashboard/routers/sectors.py +17 -0
- dojoagents/dashboard/routers/ticker.py +136 -0
- dojoagents/dashboard/routers/utility.py +38 -0
- dojoagents/dashboard/schemas/__init__.py +0 -0
- dojoagents/dashboard/schemas/agent.py +20 -0
- dojoagents/dashboard/schemas/benchmark.py +43 -0
- dojoagents/dashboard/schemas/chat_sessions.py +24 -0
- dojoagents/dashboard/schemas/common.py +7 -0
- dojoagents/dashboard/schemas/dojo_core.py +94 -0
- dojoagents/dashboard/schemas/dojo_mesh.py +49 -0
- dojoagents/dashboard/schemas/dojo_sphere.py +128 -0
- dojoagents/dashboard/schemas/domain_api.py +621 -0
- dojoagents/dashboard/schemas/freshness.py +21 -0
- dojoagents/dashboard/schemas/market.py +28 -0
- dojoagents/dashboard/schemas/portfolio.py +240 -0
- dojoagents/dashboard/schemas/sector.py +50 -0
- dojoagents/dashboard/schemas/stock.py +44 -0
- dojoagents/dashboard/schemas/stock_event.py +15 -0
- dojoagents/dashboard/schemas/stock_fin_indicators.py +16 -0
- dojoagents/dashboard/schemas/stock_income.py +27 -0
- dojoagents/dashboard/schemas/stock_kline.py +65 -0
- dojoagents/dashboard/schemas/stock_news.py +15 -0
- dojoagents/dashboard/schemas/stock_sector.py +23 -0
- dojoagents/dashboard/server.py +708 -0
- dojoagents/dashboard/services/__init__.py +17 -0
- dojoagents/dashboard/services/benchmark_store.py +281 -0
- dojoagents/dashboard/services/constituent_filter.py +65 -0
- dojoagents/dashboard/services/constituent_kline_refresh_state.py +56 -0
- dojoagents/dashboard/services/dojo_core_income.py +65 -0
- dojoagents/dashboard/services/dojo_core_pe.py +149 -0
- dojoagents/dashboard/services/dojo_core_quote.py +57 -0
- dojoagents/dashboard/services/dojo_core_search.py +141 -0
- dojoagents/dashboard/services/dojo_core_sector.py +147 -0
- dojoagents/dashboard/services/dojo_data_gateway.py +323 -0
- dojoagents/dashboard/services/dojo_sphere_service.py +50 -0
- dojoagents/dashboard/services/domain_api.py +1679 -0
- dojoagents/dashboard/services/domain_utils.py +109 -0
- dojoagents/dashboard/services/file_store_base.py +165 -0
- dojoagents/dashboard/services/fin_currency_conversion.py +194 -0
- dojoagents/dashboard/services/fin_indicators_utils.py +319 -0
- dojoagents/dashboard/services/financial_registry.py +190 -0
- dojoagents/dashboard/services/forex_store.py +164 -0
- dojoagents/dashboard/services/kline_bar_utils.py +107 -0
- dojoagents/dashboard/services/kline_segment.py +134 -0
- dojoagents/dashboard/services/kline_store.py +255 -0
- dojoagents/dashboard/services/market_refresh_jobs.py +44 -0
- dojoagents/dashboard/services/market_sector_lead.py +211 -0
- dojoagents/dashboard/services/market_stats.py +55 -0
- dojoagents/dashboard/services/portfolio_allocation.py +174 -0
- dojoagents/dashboard/services/portfolio_candidate_index.py +51 -0
- dojoagents/dashboard/services/portfolio_order_execution.py +701 -0
- dojoagents/dashboard/services/portfolio_performance.py +218 -0
- dojoagents/dashboard/services/portfolio_service.py +1002 -0
- dojoagents/dashboard/services/portfolio_store.py +551 -0
- dojoagents/dashboard/services/precompute_sector_daily.py +556 -0
- dojoagents/dashboard/services/sector_constituents.py +147 -0
- dojoagents/dashboard/services/sector_constituents_list.py +122 -0
- dojoagents/dashboard/services/sector_earnings_index.py +408 -0
- dojoagents/dashboard/services/sector_metrics_store.py +21 -0
- dojoagents/dashboard/services/sector_movers_service.py +256 -0
- dojoagents/dashboard/services/sector_precomputed_store.py +341 -0
- dojoagents/dashboard/services/sector_scope_performance.py +325 -0
- dojoagents/dashboard/services/sector_scope_performance_stats.py +76 -0
- dojoagents/dashboard/services/sector_scope_stats.py +89 -0
- dojoagents/dashboard/services/sector_store.py +407 -0
- dojoagents/dashboard/services/stock_event_store.py +43 -0
- dojoagents/dashboard/services/stock_event_utils.py +93 -0
- dojoagents/dashboard/services/stock_fin_indicators_store.py +55 -0
- dojoagents/dashboard/services/stock_income_store.py +49 -0
- dojoagents/dashboard/services/stock_income_utils.py +31 -0
- dojoagents/dashboard/services/stock_news_store.py +42 -0
- dojoagents/dashboard/services/stock_news_utils.py +159 -0
- dojoagents/dashboard/services/stock_quote_filter.py +49 -0
- dojoagents/dashboard/services/stock_sector_store.py +265 -0
- dojoagents/dashboard/services/stock_store.py +196 -0
- dojoagents/dashboard/sse.py +189 -0
- dojoagents/dashboard/static/assets/index-BsmOIoJm.css +1 -0
- dojoagents/dashboard/static/assets/index-DROMEXz5.js +17 -0
- dojoagents/dashboard/static/canvas-template.html +50 -0
- dojoagents/dashboard/static/favicon.svg +1 -0
- dojoagents/dashboard/static/icons.svg +24 -0
- dojoagents/dashboard/static/index.html +14 -0
- dojoagents/dashboard/store_manager.py +187 -0
- dojoagents/dashboard/tools/__init__.py +4 -0
- dojoagents/dashboard/tools/domain_tools.py +604 -0
- dojoagents/dashboard/tools/portfolio_tools.py +719 -0
- dojoagents/dashboard/web/dist/assets/huggingface-BkYu5Ikl.svg +11 -0
- dojoagents/dashboard/web/dist/assets/index-CqqGS94z.css +1 -0
- dojoagents/dashboard/web/dist/assets/index-D4u0x8tO.js +208 -0
- dojoagents/dashboard/web/dist/assets/logo-Bg9HUHyX.svg +20 -0
- dojoagents/dashboard/web/dist/assets/wechat-DXn3AsaS.jpg +0 -0
- dojoagents/dashboard/web/dist/favicon.svg +1 -0
- dojoagents/dashboard/web/dist/index.html +206 -0
- dojoagents/dashboard/web/dist/logo.png +0 -0
- dojoagents/dashboard/web/dist/taxonomy/stock_sector/v1.json +2046 -0
- dojoagents/data/default_portfolios/047214744248.json +405 -0
- dojoagents/data/default_portfolios/2c775a0a6cf3.json +351 -0
- dojoagents/data/default_portfolios/3ed7bd45d703.json +14 -0
- dojoagents/data/default_portfolios/76f5afffca65.json +375 -0
- dojoagents/data/default_portfolios/8d091ba038d7.json +357 -0
- dojoagents/data/default_portfolios/b9e6589d8a94.json +543 -0
- dojoagents/data/default_portfolios/index.json +24 -0
- dojoagents/data/ticker_label_aliases.jsonl +21313 -0
- dojoagents/dojo_extensions/__init__.py +1 -0
- dojoagents/dojo_extensions/base.py +36 -0
- dojoagents/dojo_extensions/registry.py +42 -0
- dojoagents/dojo_extensions/research.py +21 -0
- dojoagents/gateway/__init__.py +1 -0
- dojoagents/gateway/adapters/__init__.py +41 -0
- dojoagents/gateway/adapters/base.py +195 -0
- dojoagents/gateway/adapters/discord.py +34 -0
- dojoagents/gateway/adapters/feishu.py +48 -0
- dojoagents/gateway/adapters/slack.py +33 -0
- dojoagents/gateway/adapters/telegram.py +43 -0
- dojoagents/gateway/adapters/wechat.py +535 -0
- dojoagents/gateway/adapters/wecom.py +34 -0
- dojoagents/gateway/pairing.py +123 -0
- dojoagents/gateway/registry.py +36 -0
- dojoagents/gateway/runner.py +849 -0
- dojoagents/gateway/server.py +97 -0
- dojoagents/gateway/state.py +218 -0
- dojoagents/gateway/stream_consumer.py +108 -0
- dojoagents/logging.py +60 -0
- dojoagents/memory/__init__.py +1 -0
- dojoagents/memory/local_memory.py +69 -0
- dojoagents/memory/manager.py +148 -0
- dojoagents/memory/provider.py +29 -0
- dojoagents/memory/skill_summary.py +62 -0
- dojoagents/multi_agent/__init__.py +25 -0
- dojoagents/multi_agent/automation.py +73 -0
- dojoagents/multi_agent/models.py +62 -0
- dojoagents/multi_agent/orchestrator.py +39 -0
- dojoagents/multi_agent/pool.py +62 -0
- dojoagents/multi_agent/tools.py +61 -0
- dojoagents/multi_agent/triggers.py +62 -0
- dojoagents/packaging_hooks.py +60 -0
- dojoagents/planning/__init__.py +18 -0
- dojoagents/planning/automation.py +148 -0
- dojoagents/planning/engine.py +91 -0
- dojoagents/planning/models.py +63 -0
- dojoagents/planning/store.py +94 -0
- dojoagents/planning/tools.py +118 -0
- dojoagents/planning/triggers.py +54 -0
- dojoagents/plugins/__init__.py +13 -0
- dojoagents/plugins/built_in/__init__.py +1 -0
- dojoagents/plugins/built_in/project_guardian/scripts/audit.py +38 -0
- dojoagents/plugins/registry.py +771 -0
- dojoagents/quant/__init__.py +1 -0
- dojoagents/quant/context.py +24 -0
- dojoagents/quant/risk.py +10 -0
- dojoagents/quant/workflow.py +27 -0
- dojoagents/skills/__init__.py +1 -0
- dojoagents/skills/cache.py +81 -0
- dojoagents/skills/loader.py +7 -0
- dojoagents/skills/manager.py +173 -0
- dojoagents/tools/__init__.py +5 -0
- dojoagents/tools/agent_viz.py +1334 -0
- dojoagents/tools/code_execution_tool.py +129 -0
- dojoagents/tools/dojo_sdk_tool.py +386 -0
- dojoagents/tools/environments/__init__.py +1 -0
- dojoagents/tools/environments/base.py +60 -0
- dojoagents/tools/environments/docker.py +41 -0
- dojoagents/tools/environments/local.py +20 -0
- dojoagents/tools/environments/modal.py +20 -0
- dojoagents/tools/environments/ssh.py +24 -0
- dojoagents/tools/executor.py +108 -0
- dojoagents/tools/mcp_oauth.py +320 -0
- dojoagents/tools/mcp_tool.py +436 -0
- dojoagents/tools/plugin_manage.py +120 -0
- dojoagents/tools/process_registry.py +104 -0
- dojoagents/tools/registry.py +48 -0
- dojoagents/tools/sandbox.py +14 -0
- dojoagents/tools/skill_manage.py +352 -0
- dojoagents/tools/terminal_tool.py +71 -0
- dojoagents/tools/tools_list_tool.py +32 -0
- dojoagents/tools/web_searcher.py +281 -0
- dojoagents/utils/event_bus.py +22 -0
- dojoagents/utils/fuzzy_match.py +163 -0
- dojoagents-0.1.0.dist-info/METADATA +155 -0
- dojoagents-0.1.0.dist-info/RECORD +245 -0
- dojoagents-0.1.0.dist-info/WHEEL +5 -0
- dojoagents-0.1.0.dist-info/entry_points.txt +2 -0
- dojoagents-0.1.0.dist-info/licenses/LICENSE +201 -0
- dojoagents-0.1.0.dist-info/top_level.txt +1 -0
dojoagents/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""DojoAgents quantitative finance agent runtime."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("dojoagents")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
_version_file = Path(__file__).resolve().parents[1] / "VERSION"
|
|
11
|
+
__version__ = _version_file.read_text(encoding="utf-8").strip() if _version_file.is_file() else "0.0.0+unknown"
|
|
12
|
+
|
|
13
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Agent loop and provider abstractions."""
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Dashboard visualization protocol for the current chat UI.
|
|
2
|
+
|
|
3
|
+
The dashboard chat surface renders structured ``viz_blocks`` from tool results.
|
|
4
|
+
Unlike the legacy Canvas panel, the current React source does not render
|
|
5
|
+
``DOJO_CHART`` fenced blocks, so the dashboard prompt must steer the model
|
|
6
|
+
toward structured visualization outputs only.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
DASHBOARD_VIZ_PROTOCOL = """
|
|
12
|
+
## Dashboard Visualization Protocol
|
|
13
|
+
|
|
14
|
+
This dashboard chat renders structured `viz_blocks` from tool results. Prefer the
|
|
15
|
+
existing visualization pipeline over free-form chart code.
|
|
16
|
+
|
|
17
|
+
### Default behavior
|
|
18
|
+
|
|
19
|
+
1. Use dashboard domain tools to fetch structured data.
|
|
20
|
+
2. Reuse `viz_blocks` already attached to tool results whenever they are present.
|
|
21
|
+
3. If a chart or table is still needed, call `agent_viz_build` with the tool data
|
|
22
|
+
and an appropriate `kind` such as `bar`, `line`, `price_kline`, `table`,
|
|
23
|
+
`hbar_rank`, `donut`, or `kpi_row`.
|
|
24
|
+
4. Keep the assistant text focused on interpretation and conclusions. Let the
|
|
25
|
+
dashboard render the visualization blocks.
|
|
26
|
+
|
|
27
|
+
### Important rules
|
|
28
|
+
|
|
29
|
+
- Do NOT output `DOJO_CHART` fenced blocks.
|
|
30
|
+
- Do NOT output JavaScript, ECharts scripts, or HTML for chart rendering.
|
|
31
|
+
- Do NOT describe a chart as rendered unless a structured visualization tool has
|
|
32
|
+
already produced the matching `viz_blocks`.
|
|
33
|
+
- Prefer one comparison chart over many redundant charts when summarizing the same dataset.
|
|
34
|
+
|
|
35
|
+
### Helpful tool patterns
|
|
36
|
+
|
|
37
|
+
- For cross-market valuation comparison, prefer a single `get_market_overview`
|
|
38
|
+
call without `market` so the result covers US, CN, and HK together.
|
|
39
|
+
- For sector ranking, prefer `get_sector_movers` and render ranked bars or tables.
|
|
40
|
+
- For price trends, prefer `get_ticker_price_trends` or benchmark/ticker kline tools
|
|
41
|
+
and render `price_kline` or `line` blocks.
|
|
42
|
+
""".strip()
|
|
43
|
+
|
|
44
|
+
# Backward-compatible alias for existing imports/tests. The content intentionally
|
|
45
|
+
# reflects the current structured-viz protocol instead of the legacy canvas flow.
|
|
46
|
+
DASHBOARD_CANVAS_PROTOCOL = DASHBOARD_VIZ_PROTOCOL
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from dojoagents.logging import LOGGER
|
|
9
|
+
|
|
10
|
+
SUMMARY_PREFIX = (
|
|
11
|
+
"[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted "
|
|
12
|
+
"into the summary below. This is background reference, NOT active instructions. "
|
|
13
|
+
"Do NOT answer questions or fulfill requests mentioned in this summary; they were already addressed. "
|
|
14
|
+
"Your current task is identified in the '## Active Task' section of the summary — resume exactly from there. "
|
|
15
|
+
"Respond ONLY to the latest user message that appears AFTER this summary."
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _block_char_count(part: Any) -> int:
|
|
20
|
+
if isinstance(part, str):
|
|
21
|
+
return len(part)
|
|
22
|
+
if not isinstance(part, dict):
|
|
23
|
+
return len(str(part))
|
|
24
|
+
if "text" in part:
|
|
25
|
+
return len(str(part.get("text", "")))
|
|
26
|
+
if "toolUse" in part:
|
|
27
|
+
return len(json.dumps(part["toolUse"], ensure_ascii=False))
|
|
28
|
+
if "toolResult" in part:
|
|
29
|
+
tr = part["toolResult"]
|
|
30
|
+
texts = []
|
|
31
|
+
for block in tr.get("content", []):
|
|
32
|
+
if isinstance(block, dict) and "text" in block:
|
|
33
|
+
texts.append(str(block["text"]))
|
|
34
|
+
return len("\n".join(texts))
|
|
35
|
+
if "reasoningContent" in part:
|
|
36
|
+
rc = part["reasoningContent"]
|
|
37
|
+
return len(str(rc.get("reasoningText", {}).get("text", "")))
|
|
38
|
+
if "image" in part:
|
|
39
|
+
image = part.get("image")
|
|
40
|
+
if isinstance(image, dict):
|
|
41
|
+
source = image.get("source")
|
|
42
|
+
if isinstance(source, dict):
|
|
43
|
+
raw_bytes = source.get("bytes")
|
|
44
|
+
if isinstance(raw_bytes, (bytes, bytearray)):
|
|
45
|
+
return len(raw_bytes)
|
|
46
|
+
if isinstance(raw_bytes, str):
|
|
47
|
+
return len(raw_bytes)
|
|
48
|
+
location = source.get("location")
|
|
49
|
+
if isinstance(location, dict) and str(location.get("type") or "").strip():
|
|
50
|
+
return len(str(location["type"]))
|
|
51
|
+
return 4096
|
|
52
|
+
return len(json.dumps(part, ensure_ascii=False))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _estimate_tokens_rough(messages: list[dict[str, Any]]) -> int:
|
|
56
|
+
"""Rough estimate of tokens based on character length (approx 4 chars per token)."""
|
|
57
|
+
char_count = 0
|
|
58
|
+
for msg in messages:
|
|
59
|
+
content = msg.get("content") or ""
|
|
60
|
+
if isinstance(content, str):
|
|
61
|
+
char_count += len(content)
|
|
62
|
+
elif isinstance(content, list):
|
|
63
|
+
for part in content:
|
|
64
|
+
char_count += _block_char_count(part)
|
|
65
|
+
|
|
66
|
+
for tc in msg.get("tool_calls") or []:
|
|
67
|
+
if isinstance(tc, dict):
|
|
68
|
+
char_count += len(str(tc.get("function", {}).get("arguments", "")))
|
|
69
|
+
return char_count // 4
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def flatten_messages_for_compress(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
73
|
+
flat: list[dict[str, Any]] = []
|
|
74
|
+
for msg in messages:
|
|
75
|
+
role = str(msg.get("role") or "")
|
|
76
|
+
content = msg.get("content")
|
|
77
|
+
if isinstance(content, list):
|
|
78
|
+
text_parts: list[str] = []
|
|
79
|
+
tool_calls: list[dict[str, Any]] = []
|
|
80
|
+
for part in content:
|
|
81
|
+
if isinstance(part, dict) and "text" in part:
|
|
82
|
+
text_parts.append(str(part["text"]))
|
|
83
|
+
elif isinstance(part, dict) and "toolUse" in part:
|
|
84
|
+
tu = part["toolUse"]
|
|
85
|
+
tool_calls.append(
|
|
86
|
+
{
|
|
87
|
+
"id": tu.get("toolUseId"),
|
|
88
|
+
"type": "function",
|
|
89
|
+
"function": {
|
|
90
|
+
"name": tu.get("name"),
|
|
91
|
+
"arguments": json.dumps(tu.get("input", {}), ensure_ascii=False),
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
elif isinstance(part, dict) and "toolResult" in part:
|
|
96
|
+
tr = part["toolResult"]
|
|
97
|
+
result_text = ""
|
|
98
|
+
for block in tr.get("content", []):
|
|
99
|
+
if isinstance(block, dict) and "text" in block:
|
|
100
|
+
result_text += str(block["text"])
|
|
101
|
+
flat.append(
|
|
102
|
+
{
|
|
103
|
+
"role": "tool",
|
|
104
|
+
"tool_call_id": tr.get("toolUseId"),
|
|
105
|
+
"name": tr.get("name"),
|
|
106
|
+
"content": result_text,
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
entry: dict[str, Any] = {"role": role, "content": "\n".join(text_parts)}
|
|
110
|
+
if tool_calls:
|
|
111
|
+
entry["tool_calls"] = tool_calls
|
|
112
|
+
if text_parts or tool_calls:
|
|
113
|
+
flat.append(entry)
|
|
114
|
+
continue
|
|
115
|
+
entry = {"role": role, "content": str(content or "")}
|
|
116
|
+
if msg.get("tool_calls"):
|
|
117
|
+
entry["tool_calls"] = msg["tool_calls"]
|
|
118
|
+
if msg.get("tool_call_id"):
|
|
119
|
+
entry["tool_call_id"] = msg["tool_call_id"]
|
|
120
|
+
if msg.get("name"):
|
|
121
|
+
entry["name"] = msg["name"]
|
|
122
|
+
flat.append(entry)
|
|
123
|
+
return flat
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def messages_to_strands(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
127
|
+
strands: list[dict[str, Any]] = []
|
|
128
|
+
for msg in messages:
|
|
129
|
+
role = str(msg.get("role") or "")
|
|
130
|
+
if role == "tool":
|
|
131
|
+
strands.append(
|
|
132
|
+
{
|
|
133
|
+
"role": "user",
|
|
134
|
+
"content": [
|
|
135
|
+
{
|
|
136
|
+
"toolResult": {
|
|
137
|
+
"status": "success",
|
|
138
|
+
"toolUseId": msg.get("tool_call_id"),
|
|
139
|
+
"name": msg.get("name") or "tool",
|
|
140
|
+
"content": [{"text": str(msg.get("content") or "")}],
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
continue
|
|
147
|
+
blocks: list[dict[str, Any]] = []
|
|
148
|
+
content = msg.get("content")
|
|
149
|
+
if isinstance(content, str) and content:
|
|
150
|
+
blocks.append({"text": content})
|
|
151
|
+
for tc in msg.get("tool_calls") or []:
|
|
152
|
+
if not isinstance(tc, dict):
|
|
153
|
+
continue
|
|
154
|
+
func = tc.get("function") or {}
|
|
155
|
+
args = func.get("arguments")
|
|
156
|
+
if isinstance(args, str):
|
|
157
|
+
try:
|
|
158
|
+
args_dict = json.loads(args)
|
|
159
|
+
except json.JSONDecodeError:
|
|
160
|
+
args_dict = {"raw": args}
|
|
161
|
+
else:
|
|
162
|
+
args_dict = args or {}
|
|
163
|
+
blocks.append(
|
|
164
|
+
{
|
|
165
|
+
"toolUse": {
|
|
166
|
+
"toolUseId": tc.get("id"),
|
|
167
|
+
"name": func.get("name"),
|
|
168
|
+
"input": args_dict,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
strands.append({"role": role, "content": blocks})
|
|
173
|
+
return strands
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _truncate_tool_call_args_json(args: str, head_chars: int = 150) -> str:
|
|
177
|
+
try:
|
|
178
|
+
parsed = json.loads(args)
|
|
179
|
+
except (ValueError, TypeError):
|
|
180
|
+
return args
|
|
181
|
+
|
|
182
|
+
def _shrink(obj: Any) -> Any:
|
|
183
|
+
if isinstance(obj, str):
|
|
184
|
+
if len(obj) > head_chars:
|
|
185
|
+
return obj[:head_chars] + "...[truncated]"
|
|
186
|
+
return obj
|
|
187
|
+
if isinstance(obj, dict):
|
|
188
|
+
return {k: _shrink(v) for k, v in obj.items()}
|
|
189
|
+
if isinstance(obj, list):
|
|
190
|
+
return [_shrink(v) for v in obj]
|
|
191
|
+
return obj
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
return json.dumps(_shrink(parsed), ensure_ascii=False)
|
|
195
|
+
except Exception:
|
|
196
|
+
return args
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _summarize_tool_result(tool_name: str, tool_args: str, tool_content: str) -> str:
|
|
200
|
+
try:
|
|
201
|
+
args = json.loads(tool_args) if tool_args else {}
|
|
202
|
+
except (json.JSONDecodeError, TypeError):
|
|
203
|
+
args = {}
|
|
204
|
+
|
|
205
|
+
content = tool_content or ""
|
|
206
|
+
content_len = len(content)
|
|
207
|
+
line_count = content.count("\n") + 1 if content.strip() else 0
|
|
208
|
+
|
|
209
|
+
if tool_name == "terminal":
|
|
210
|
+
cmd = args.get("command", "")
|
|
211
|
+
if len(cmd) > 60:
|
|
212
|
+
cmd = cmd[:57] + "..."
|
|
213
|
+
exit_match = re.search(r'"exit_code"\s*:\s*(-?\d+)', content)
|
|
214
|
+
exit_code = exit_match.group(1) if exit_match else "?"
|
|
215
|
+
return f"[terminal] ran `{cmd}` -> exit {exit_code}, {line_count} lines output"
|
|
216
|
+
|
|
217
|
+
if tool_name == "read_file":
|
|
218
|
+
path = args.get("path", "?")
|
|
219
|
+
return f"[read_file] read {path} ({content_len:,} chars)"
|
|
220
|
+
|
|
221
|
+
if tool_name == "write_file":
|
|
222
|
+
path = args.get("path", "?")
|
|
223
|
+
written_lines = args.get("content", "").count("\n") + 1 if args.get("content") else "?"
|
|
224
|
+
return f"[write_file] wrote to {path} ({written_lines} lines)"
|
|
225
|
+
|
|
226
|
+
if tool_name == "patch":
|
|
227
|
+
path = args.get("path", "?")
|
|
228
|
+
return f"[patch] patched {path} ({content_len:,} chars result)"
|
|
229
|
+
|
|
230
|
+
if tool_name == "web_search":
|
|
231
|
+
query = args.get("query", "?")
|
|
232
|
+
return f"[web_search] query='{query}' ({content_len:,} chars result)"
|
|
233
|
+
|
|
234
|
+
return f"[{tool_name}] executed ({content_len:,} chars result)"
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class ContextCompressor:
|
|
238
|
+
def __init__(
|
|
239
|
+
self,
|
|
240
|
+
protect_first_n: int = 3,
|
|
241
|
+
protect_last_n: int = 8,
|
|
242
|
+
) -> None:
|
|
243
|
+
self.protect_first_n = protect_first_n
|
|
244
|
+
self.protect_last_n = protect_last_n
|
|
245
|
+
self._previous_summary: str | None = None
|
|
246
|
+
|
|
247
|
+
def prune_old_tool_results(self, messages: list[dict[str, Any]], protect_tail_count: int) -> list[dict[str, Any]]:
|
|
248
|
+
if not messages:
|
|
249
|
+
return messages
|
|
250
|
+
|
|
251
|
+
flat = flatten_messages_for_compress(messages)
|
|
252
|
+
result = [dict(m) for m in flat]
|
|
253
|
+
prune_boundary = len(result) - protect_tail_count
|
|
254
|
+
if prune_boundary <= 0:
|
|
255
|
+
return messages
|
|
256
|
+
|
|
257
|
+
call_id_to_tool: dict[str, tuple[str, str]] = {}
|
|
258
|
+
for msg in result:
|
|
259
|
+
if msg.get("role") == "assistant":
|
|
260
|
+
for tc in msg.get("tool_calls") or []:
|
|
261
|
+
if isinstance(tc, dict):
|
|
262
|
+
cid = str(tc.get("id", ""))
|
|
263
|
+
fn = tc.get("function", {})
|
|
264
|
+
call_id_to_tool[cid] = (fn.get("name", "unknown"), fn.get("arguments", ""))
|
|
265
|
+
|
|
266
|
+
seen_hashes: set[str] = set()
|
|
267
|
+
for i in range(prune_boundary):
|
|
268
|
+
msg = result[i]
|
|
269
|
+
role = msg.get("role")
|
|
270
|
+
|
|
271
|
+
if role == "tool":
|
|
272
|
+
content = msg.get("content", "")
|
|
273
|
+
if isinstance(content, str) and len(content) > 150:
|
|
274
|
+
digest = hashlib.md5(content.encode("utf-8")).hexdigest()
|
|
275
|
+
if digest in seen_hashes:
|
|
276
|
+
result[i] = {**msg, "content": "[Duplicate tool output omitted]"}
|
|
277
|
+
else:
|
|
278
|
+
seen_hashes.add(digest)
|
|
279
|
+
call_id = str(msg.get("tool_call_id", ""))
|
|
280
|
+
t_name, t_args = call_id_to_tool.get(call_id, ("unknown", ""))
|
|
281
|
+
result[i] = {**msg, "content": _summarize_tool_result(t_name, t_args, content)}
|
|
282
|
+
|
|
283
|
+
elif role == "assistant" and msg.get("tool_calls"):
|
|
284
|
+
new_tcs = []
|
|
285
|
+
for tc in msg["tool_calls"]:
|
|
286
|
+
if isinstance(tc, dict):
|
|
287
|
+
args = tc.get("function", {}).get("arguments", "")
|
|
288
|
+
if len(args) > 300:
|
|
289
|
+
tc = {
|
|
290
|
+
**tc,
|
|
291
|
+
"function": {
|
|
292
|
+
**tc["function"],
|
|
293
|
+
"arguments": _truncate_tool_call_args_json(args),
|
|
294
|
+
},
|
|
295
|
+
}
|
|
296
|
+
new_tcs.append(tc)
|
|
297
|
+
result[i] = {**msg, "tool_calls": new_tcs}
|
|
298
|
+
|
|
299
|
+
if messages and isinstance(messages[0].get("content"), list):
|
|
300
|
+
return messages_to_strands(result)
|
|
301
|
+
return result
|
|
302
|
+
|
|
303
|
+
async def compress(
|
|
304
|
+
self,
|
|
305
|
+
messages: list[dict[str, Any]],
|
|
306
|
+
llm_provider: Any,
|
|
307
|
+
model: str,
|
|
308
|
+
memory_manager: Any = None,
|
|
309
|
+
session_id: str = "",
|
|
310
|
+
) -> list[dict[str, Any]]:
|
|
311
|
+
"""Compress middle turns using LLM. Caller decides when to invoke."""
|
|
312
|
+
strands_input = bool(messages) and isinstance(messages[0].get("content"), list)
|
|
313
|
+
working = flatten_messages_for_compress(messages)
|
|
314
|
+
pruned_messages = self.prune_old_tool_results(working, self.protect_last_n)
|
|
315
|
+
|
|
316
|
+
head_count = min(self.protect_first_n, len(pruned_messages))
|
|
317
|
+
tail_count = min(self.protect_last_n, len(pruned_messages) - head_count)
|
|
318
|
+
middle_count = len(pruned_messages) - head_count - tail_count
|
|
319
|
+
if middle_count <= 2:
|
|
320
|
+
return messages_to_strands(pruned_messages) if strands_input else pruned_messages
|
|
321
|
+
|
|
322
|
+
head = pruned_messages[:head_count]
|
|
323
|
+
middle = pruned_messages[head_count : head_count + middle_count]
|
|
324
|
+
tail = pruned_messages[head_count + middle_count :]
|
|
325
|
+
|
|
326
|
+
middle_prompt = (
|
|
327
|
+
"You are a context compression assistant. Analyze the dialogue sequence below and extract two things:\n"
|
|
328
|
+
"1. A compact dialogue summary of the middle turns for immediate context continuation.\n"
|
|
329
|
+
"2. Key long-term facts, preferences, user habits, and general workflows that should be saved in the agent's long-term memory.\n\n"
|
|
330
|
+
"Mark each prior user task as COMPLETED unless the latest user message explicitly continues it.\n"
|
|
331
|
+
"The latest user message defines the ONLY active task — do not carry forward unfinished work from older turns.\n\n"
|
|
332
|
+
"Format your output exactly like this:\n"
|
|
333
|
+
"[CONSOLIDATION SUMMARY]\n"
|
|
334
|
+
"<compact summary of dialogue sequence>\n"
|
|
335
|
+
"[LONG-TERM FACTS]\n"
|
|
336
|
+
"<extracted long-term facts and workflows>\n\n"
|
|
337
|
+
"Conversation history to compact:\n"
|
|
338
|
+
)
|
|
339
|
+
if self._previous_summary:
|
|
340
|
+
middle_prompt += f"Previous compaction summary:\n{self._previous_summary}\n\n"
|
|
341
|
+
|
|
342
|
+
for m in middle:
|
|
343
|
+
role = m.get("role", "")
|
|
344
|
+
content = m.get("content") or ""
|
|
345
|
+
tcs = m.get("tool_calls")
|
|
346
|
+
middle_prompt += f"[{role}]: {content}\n"
|
|
347
|
+
if tcs:
|
|
348
|
+
middle_prompt += f"Tool Calls: {json.dumps(tcs)}\n"
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
summary_result = await llm_provider.chat(
|
|
352
|
+
messages=[{"role": "user", "content": middle_prompt}],
|
|
353
|
+
tools=[],
|
|
354
|
+
model=model,
|
|
355
|
+
)
|
|
356
|
+
content = summary_result.content
|
|
357
|
+
if "[LONG-TERM FACTS]" in content:
|
|
358
|
+
parts = content.split("[LONG-TERM FACTS]")
|
|
359
|
+
summary_content = parts[0].replace("[CONSOLIDATION SUMMARY]", "").strip()
|
|
360
|
+
facts_part = parts[1].strip()
|
|
361
|
+
else:
|
|
362
|
+
summary_content = content.replace("[CONSOLIDATION SUMMARY]", "").strip()
|
|
363
|
+
facts_part = ""
|
|
364
|
+
|
|
365
|
+
self._previous_summary = summary_content
|
|
366
|
+
if facts_part and memory_manager and session_id:
|
|
367
|
+
await memory_manager.save_memory(session_id, facts_part)
|
|
368
|
+
except Exception:
|
|
369
|
+
LOGGER.exception("Failed to generate compaction summary")
|
|
370
|
+
summary_content = "[Compacted due to token limit: summary generation failed]"
|
|
371
|
+
|
|
372
|
+
summary_message = {
|
|
373
|
+
"role": "system",
|
|
374
|
+
"content": f"{SUMMARY_PREFIX}\n\n## Compacted Summary\n{summary_content}",
|
|
375
|
+
}
|
|
376
|
+
compressed = [*head, summary_message, *tail]
|
|
377
|
+
return messages_to_strands(compressed) if strands_input else compressed
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
_MAX_CONTEXT_RE = re.compile(r"maximum context length is (\d+)", re.IGNORECASE)
|
|
6
|
+
_REQUESTED_TOKENS_RE = re.compile(r"requested (\d+) tokens", re.IGNORECASE)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ContextLengthExceededError(Exception):
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
message: str,
|
|
13
|
+
*,
|
|
14
|
+
max_context: int | None = None,
|
|
15
|
+
requested_tokens: int | None = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.max_context = max_context
|
|
19
|
+
self.requested_tokens = requested_tokens
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_context_length_error(message: str) -> tuple[int | None, int | None]:
|
|
23
|
+
max_match = _MAX_CONTEXT_RE.search(message)
|
|
24
|
+
req_match = _REQUESTED_TOKENS_RE.search(message)
|
|
25
|
+
max_context = int(max_match.group(1)) if max_match else None
|
|
26
|
+
requested = int(req_match.group(1)) if req_match else None
|
|
27
|
+
return max_context, requested
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Precise dashboard tool calling guidelines injected into the agent system prompt."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
DASHBOARD_TOOL_PROTOCOL = """
|
|
6
|
+
## Dashboard Tool Calling Protocol (MANDATORY)
|
|
7
|
+
|
|
8
|
+
Pick the workflow by user intent. Do NOT default to search_company_ticker or web_search for stock picking.
|
|
9
|
+
|
|
10
|
+
### Multi-turn sessions (CRITICAL — avoid intent drift)
|
|
11
|
+
|
|
12
|
+
Each user message is a **new, independent task**. Prior turns are closed context.
|
|
13
|
+
|
|
14
|
+
- If the **latest message** asks to **analyze / 分析 / 解读 / 怎么样** an existing portfolio or its candidates:
|
|
15
|
+
use read + financial tools only (`portfolio_read_search`, `portfolio_read_detail`, `get_ticker_financials`, …).
|
|
16
|
+
**Do NOT** call `portfolio_write_create`, batch add candidates, or `portfolio_eval_submit`.
|
|
17
|
+
- If the **latest message** asks to **create / 创建 / 选股 / 建组合**:
|
|
18
|
+
use the portfolio build workflow below.
|
|
19
|
+
- Never resume a prior turn's unfinished create/build work unless the **current message** explicitly asks.
|
|
20
|
+
|
|
21
|
+
When session history mentions an old portfolio task, treat it as **already done** unless the user repeats that request now.
|
|
22
|
+
|
|
23
|
+
### Task routing (choose ONE path per message)
|
|
24
|
+
|
|
25
|
+
| User intent | Required tools (in order) | Do NOT use |
|
|
26
|
+
|-------------|---------------------------|------------|
|
|
27
|
+
| Theme / concept / industry basket (具身智能, 机器人, 半导体, AI…) | `search_sector_taxonomy` → `filter_sector_constituents` (per market) → optional `get_ticker_financials` batch → portfolio writes | `search_company_ticker`, `web_search` as primary discovery |
|
|
28
|
+
| Sector analysis / compare industries | `get_taxonomy_tree` or `search_sector_taxonomy` → `get_sector_analysis` | Guessing sector ids |
|
|
29
|
+
| Full-market screen (市值/PE/涨跌幅 filters, no specific sector) | `screen_market_stocks` per market → optional `get_ticker_financials` | `filter_sector_constituents` without taxonomy match |
|
|
30
|
+
| Resolve one company name → ticker | `search_company_ticker` (single q, known name) | Repeated keyword searches |
|
|
31
|
+
| Analyze existing portfolio / 候选池成分分析 | `portfolio_read_search` → `portfolio_read_detail` → `get_ticker_financials` batch → answer | `portfolio_write_create`, add candidates, eval_submit |
|
|
32
|
+
| Single stock deep dive | `get_ticker_realtime_quote`, `get_ticker_financials`, `get_ticker_price_trends` | — |
|
|
33
|
+
|
|
34
|
+
### Analyze portfolio candidates (分析候选池 / 成分股怎么样)
|
|
35
|
+
|
|
36
|
+
Read-only workflow — no portfolio writes:
|
|
37
|
+
|
|
38
|
+
1. `portfolio_read_search` with portfolio name from the user
|
|
39
|
+
2. `portfolio_read_detail` for candidates list
|
|
40
|
+
3. Batch `get_ticker_financials` (and optional quotes) on candidate tickers
|
|
41
|
+
4. Summarize quality, valuation, risks — **stop**. No create, no eval_submit.
|
|
42
|
+
|
|
43
|
+
### Theme / concept stock picking (e.g. 具身智能, 高息, 半导体)
|
|
44
|
+
|
|
45
|
+
Concept names are NOT tickers. `search_company_ticker("具身智能")` or `search_company_ticker("Tesla")` will NOT return a complete universe.
|
|
46
|
+
|
|
47
|
+
**Required workflow:**
|
|
48
|
+
|
|
49
|
+
1. `search_sector_taxonomy` with the user's concept and close synonyms
|
|
50
|
+
(e.g. 具身智能 → also try 机器人, 自动化, robotics, industrial automation).
|
|
51
|
+
2. From matches, pick the best L3 sector (`best_match` or highest `match_score`).
|
|
52
|
+
3. For each target market (`us`, `cn`, `hk`), call `filter_sector_constituents`
|
|
53
|
+
with `sector_path_id` or the three ids from step 2 — do NOT pass sector names.
|
|
54
|
+
4. Apply numeric filters on the result set:
|
|
55
|
+
- market cap: use `min_market_cap` in `screen_market_stocks` only if you pivoted to market-wide screen;
|
|
56
|
+
otherwise filter constituent rows by `market_cap` field.
|
|
57
|
+
- profitability: batch `get_ticker_financials` on candidate tickers, keep net_profit > 0.
|
|
58
|
+
5. Portfolio watchlist (选股/候选池): `portfolio_write_create` → `portfolio_write_add_candidates` →
|
|
59
|
+
`portfolio_read_detail` (read `eval_summary.candidate_count`) → `portfolio_eval_submit` with **min_candidate_count**.
|
|
60
|
+
|
|
61
|
+
**Eval rules (avoid retry loops):**
|
|
62
|
+
- `min_candidates_by_market` must come from `portfolio_read_detail.eval_summary`, NOT from pre-filter estimates.
|
|
63
|
+
- Do NOT invent per-market minimums (e.g. US≥40) unless the user explicitly asked for a count.
|
|
64
|
+
- If `add_result.skipped_duplicates` is non-empty, those tickers did NOT increase the count — pick new symbols.
|
|
65
|
+
- After eval failure: fix only the gap; do NOT re-print the full portfolio report.
|
|
66
|
+
|
|
67
|
+
**Optional:** `get_sector_analysis` on the chosen path for sector-level context before picking names.
|
|
68
|
+
|
|
69
|
+
### Portfolio: candidates vs positions (CRITICAL)
|
|
70
|
+
|
|
71
|
+
| Concept | UI label | Meaning | Tool | Eval field |
|
|
72
|
+
|---------|----------|---------|------|------------|
|
|
73
|
+
| Watchlist | 候选股 | Track symbols, no capital spent | `portfolio_write_add_candidate(s)` | `min_candidate_count` |
|
|
74
|
+
| Filled buy | 持仓 / 建仓 | Spend capital at price × qty | `portfolio_write_create_order(s)` | `min_position_count` |
|
|
75
|
+
|
|
76
|
+
**User says 建仓 / 买入 / 按成本价 / 创建交易 / 持仓页面截图 with shares & cost:**
|
|
77
|
+
1. `portfolio_read_search` or `portfolio_read_list` → target portfolio_id
|
|
78
|
+
2. For each row: `portfolio_write_create_order` (or batch `portfolio_write_create_orders`)
|
|
79
|
+
with `order_side=buy`, `price`=cost/limit, `qty`=shares, optional `order_time`=open date
|
|
80
|
+
3. `portfolio_read_detail` → verify `eval_summary.position_count` (NOT candidate_count)
|
|
81
|
+
4. `portfolio_eval_submit` with **min_position_count** matching filled positions
|
|
82
|
+
|
|
83
|
+
**FORBIDDEN for 建仓:** `portfolio_write_add_candidate`, `portfolio_write_add_holding`, `portfolio_write_add_holdings`
|
|
84
|
+
(these only add 候选股; they never buy or set cost).
|
|
85
|
+
|
|
86
|
+
**Theme basket without 建仓:** use add_candidates only — positions stay 0 until user asks to buy.
|
|
87
|
+
|
|
88
|
+
### Sector taxonomy ids
|
|
89
|
+
|
|
90
|
+
1. `search_sector_taxonomy` with the user's concept (synonyms auto-expanded: 具身智能 → 机器人, robotics…).
|
|
91
|
+
2. Copy `sector_path_id` OR `level1_id` + `level2_id` + `level3_id` verbatim from `best_match` — exact ID lookup, no guessing.
|
|
92
|
+
3. `filter_sector_constituents` with those ids + `market` + `scope: "L3"`.
|
|
93
|
+
4. `get_sector_analysis` with the same ids when sector-level stats are needed.
|
|
94
|
+
|
|
95
|
+
### search_company_ticker — ONLY for single-name resolution
|
|
96
|
+
|
|
97
|
+
Use when the user names ONE company or ticker (Apple, 茅台, 0700.HK).
|
|
98
|
+
FORBIDDEN as stock-universe builder:
|
|
99
|
+
- thematic keywords (具身智能, 机器人, embodied AI)
|
|
100
|
+
- looping famous names (NVIDIA, Tesla, BYD) to assemble a concept basket
|
|
101
|
+
- replacing `filter_sector_constituents` or `screen_market_stocks`
|
|
102
|
+
|
|
103
|
+
### web_search — supplementary only
|
|
104
|
+
|
|
105
|
+
Use for news/macro context AFTER dashboard tools return candidates.
|
|
106
|
+
FORBIDDEN as the primary way to discover investable tickers when sector/screen tools exist.
|
|
107
|
+
|
|
108
|
+
### Portfolio tools
|
|
109
|
+
|
|
110
|
+
**Watchlist / 候选股:** create → add_candidates → read_detail → eval_submit (min_candidate_count)
|
|
111
|
+
**建仓 / 买入:** read_search → create_order(s) with price+qty → read_detail → eval_submit (min_position_count)
|
|
112
|
+
**Delete:** read_list → write_delete → done (no read_detail, no eval_submit)
|
|
113
|
+
|
|
114
|
+
### Batch calls
|
|
115
|
+
|
|
116
|
+
- `get_ticker_realtime_quote` / `get_ticker_financials`: pass all tickers in one `tickers` array (≤50).
|
|
117
|
+
""".strip()
|