tradingcodex 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.
- apps/__init__.py +1 -0
- apps/audit/__init__.py +1 -0
- apps/audit/admin.py +6 -0
- apps/audit/apps.py +8 -0
- apps/audit/migrations/0001_initial.py +35 -0
- apps/audit/migrations/__init__.py +0 -0
- apps/audit/models.py +22 -0
- apps/harness/__init__.py +1 -0
- apps/harness/admin.py +6 -0
- apps/harness/apps.py +8 -0
- apps/harness/migrations/0001_initial.py +35 -0
- apps/harness/migrations/__init__.py +0 -0
- apps/harness/models.py +22 -0
- apps/harness/templatetags/__init__.py +1 -0
- apps/integrations/__init__.py +1 -0
- apps/integrations/admin.py +6 -0
- apps/integrations/apps.py +8 -0
- apps/integrations/migrations/0001_initial.py +29 -0
- apps/integrations/migrations/__init__.py +0 -0
- apps/integrations/models.py +16 -0
- apps/integrations/services.py +31 -0
- apps/mcp/__init__.py +1 -0
- apps/mcp/admin.py +20 -0
- apps/mcp/apps.py +8 -0
- apps/mcp/migrations/0001_initial.py +168 -0
- apps/mcp/migrations/__init__.py +0 -0
- apps/mcp/models.py +154 -0
- apps/mcp/services.py +327 -0
- apps/orders/__init__.py +1 -0
- apps/orders/admin.py +6 -0
- apps/orders/apps.py +8 -0
- apps/orders/migrations/0001_initial.py +79 -0
- apps/orders/migrations/__init__.py +0 -0
- apps/orders/models.py +66 -0
- apps/orders/services.py +107 -0
- apps/policy/__init__.py +1 -0
- apps/policy/admin.py +6 -0
- apps/policy/apps.py +8 -0
- apps/policy/migrations/0001_initial.py +75 -0
- apps/policy/migrations/__init__.py +0 -0
- apps/policy/models.py +61 -0
- apps/policy/services.py +110 -0
- apps/portfolio/__init__.py +1 -0
- apps/portfolio/admin.py +6 -0
- apps/portfolio/apps.py +8 -0
- apps/portfolio/migrations/0001_initial.py +67 -0
- apps/portfolio/migrations/__init__.py +0 -0
- apps/portfolio/models.py +53 -0
- apps/research/__init__.py +1 -0
- apps/research/admin.py +1 -0
- apps/research/apps.py +8 -0
- apps/research/migrations/__init__.py +0 -0
- apps/research/models.py +1 -0
- apps/workflows/__init__.py +1 -0
- apps/workflows/admin.py +6 -0
- apps/workflows/apps.py +8 -0
- apps/workflows/migrations/0001_initial.py +51 -0
- apps/workflows/migrations/__init__.py +0 -0
- apps/workflows/models.py +44 -0
- tradingcodex-0.1.0.dist-info/METADATA +337 -0
- tradingcodex-0.1.0.dist-info/RECORD +254 -0
- tradingcodex-0.1.0.dist-info/WHEEL +5 -0
- tradingcodex-0.1.0.dist-info/entry_points.txt +2 -0
- tradingcodex-0.1.0.dist-info/licenses/LICENSE +202 -0
- tradingcodex-0.1.0.dist-info/licenses/NOTICE +24 -0
- tradingcodex-0.1.0.dist-info/top_level.txt +4 -0
- tradingcodex_cli/__init__.py +1 -0
- tradingcodex_cli/__main__.py +124 -0
- tradingcodex_cli/commands/__init__.py +2 -0
- tradingcodex_cli/commands/bootstrap.py +157 -0
- tradingcodex_cli/commands/db.py +36 -0
- tradingcodex_cli/commands/doctor.py +230 -0
- tradingcodex_cli/commands/mcp.py +186 -0
- tradingcodex_cli/commands/orders.py +89 -0
- tradingcodex_cli/commands/policy.py +21 -0
- tradingcodex_cli/commands/profile.py +110 -0
- tradingcodex_cli/commands/research.py +76 -0
- tradingcodex_cli/commands/skills.py +93 -0
- tradingcodex_cli/commands/strategies.py +67 -0
- tradingcodex_cli/commands/subagents.py +106 -0
- tradingcodex_cli/commands/utils.py +134 -0
- tradingcodex_cli/commands/workspaces.py +53 -0
- tradingcodex_cli/generator.py +234 -0
- tradingcodex_cli/mcp_stdio.py +26 -0
- tradingcodex_cli/service_autostart.py +127 -0
- tradingcodex_service/__init__.py +5 -0
- tradingcodex_service/admin.py +1 -0
- tradingcodex_service/api.py +486 -0
- tradingcodex_service/application/__init__.py +2 -0
- tradingcodex_service/application/agents.py +1470 -0
- tradingcodex_service/application/audit.py +71 -0
- tradingcodex_service/application/common.py +88 -0
- tradingcodex_service/application/components.py +299 -0
- tradingcodex_service/application/harness.py +747 -0
- tradingcodex_service/application/markdown_preview.py +179 -0
- tradingcodex_service/application/orders.py +404 -0
- tradingcodex_service/application/policy.py +150 -0
- tradingcodex_service/application/portfolio.py +166 -0
- tradingcodex_service/application/research.py +356 -0
- tradingcodex_service/application/runtime.py +321 -0
- tradingcodex_service/asgi.py +6 -0
- tradingcodex_service/mcp_http.py +59 -0
- tradingcodex_service/mcp_runtime.py +565 -0
- tradingcodex_service/settings.py +91 -0
- tradingcodex_service/templates/web/activity.html +24 -0
- tradingcodex_service/templates/web/agent_skills.html +111 -0
- tradingcodex_service/templates/web/agents.html +121 -0
- tradingcodex_service/templates/web/base.html +150 -0
- tradingcodex_service/templates/web/dashboard.html +109 -0
- tradingcodex_service/templates/web/fragments/role_inspector.html +81 -0
- tradingcodex_service/templates/web/fragments/starter_prompt.html +24 -0
- tradingcodex_service/templates/web/fragments/topology_canvas.html +85 -0
- tradingcodex_service/templates/web/harness.html +87 -0
- tradingcodex_service/templates/web/mcp_router.html +250 -0
- tradingcodex_service/templates/web/orders.html +68 -0
- tradingcodex_service/templates/web/policy.html +81 -0
- tradingcodex_service/templates/web/portfolio.html +52 -0
- tradingcodex_service/templates/web/research.html +73 -0
- tradingcodex_service/templates/web/starter_prompt.html +40 -0
- tradingcodex_service/templates/web/strategies.html +74 -0
- tradingcodex_service/urls.py +42 -0
- tradingcodex_service/version.py +1 -0
- tradingcodex_service/web.py +885 -0
- tradingcodex_service/wsgi.py +6 -0
- workspace_templates/__init__.py +1 -0
- workspace_templates/modules/audit/files/.tradingcodex/audit/README.md +5 -0
- workspace_templates/modules/audit/files/trading/audit/.gitkeep +1 -0
- workspace_templates/modules/audit/module.json +16 -0
- workspace_templates/modules/codex-base/files/.codex/config.toml +272 -0
- workspace_templates/modules/codex-base/files/.codex/hooks/tradingcodex_hook.py +173 -0
- workspace_templates/modules/codex-base/files/.codex/hooks.json +105 -0
- workspace_templates/modules/codex-base/files/.codex/prompts/base_instructions/head-manager.md +129 -0
- workspace_templates/modules/codex-base/files/.codex/rules/tradingcodex.rules +50 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/capabilities.yaml +56 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/cli.py +16 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/config.yaml +53 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/policies/policy-bindings.yaml +16 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/policies/principals.yaml +17 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/policies/roles.yaml +27 -0
- workspace_templates/modules/codex-base/files/AGENTS.md +56 -0
- workspace_templates/modules/codex-base/files/pyproject.toml +13 -0
- workspace_templates/modules/codex-base/files/tcx +35 -0
- workspace_templates/modules/codex-base/module.json +17 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/policies/access-policies.yaml +39 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/policies/restricted-list.yaml +6 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/approval_receipt.schema.json +14 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/audit_event.schema.json +11 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/evidence_pack.schema.json +16 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/execution_result.schema.json +12 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/fundamental_report.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/news_report.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/order_intent.schema.json +30 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/portfolio_review.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/postmortem_report.schema.json +14 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/risk_report.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/technical_report.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/thesis.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/valuation.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/scripts/validate-order-intent.py +15 -0
- workspace_templates/modules/enforcement-guardrails/module.json +19 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/execution-operator.toml +71 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/fundamental-analyst.toml +62 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/instrument-analyst.toml +64 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/macro-analyst.toml +64 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/news-analyst.toml +63 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/portfolio-manager.toml +62 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/risk-manager.toml +65 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/technical-analyst.toml +63 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/valuation-analyst.toml +59 -0
- workspace_templates/modules/fixed-subagents/files/.tradingcodex/mainagent/head-manager.yaml +66 -0
- workspace_templates/modules/fixed-subagents/files/.tradingcodex/mainagent/skill-change-proposals/.gitkeep +1 -0
- workspace_templates/modules/fixed-subagents/files/.tradingcodex/mainagent/subagent-registry.yaml +56 -0
- workspace_templates/modules/fixed-subagents/module.json +23 -0
- workspace_templates/modules/guidance-guardrails/files/.tradingcodex/guidance/guardrails.md +17 -0
- workspace_templates/modules/guidance-guardrails/files/.tradingcodex/guidance/task-quality-checklist.md +37 -0
- workspace_templates/modules/guidance-guardrails/module.json +18 -0
- workspace_templates/modules/information-barriers/files/.tradingcodex/policies/information-barriers.yaml +211 -0
- workspace_templates/modules/information-barriers/files/.tradingcodex/secrets.md +9 -0
- workspace_templates/modules/information-barriers/files/trading/approvals/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/market-data/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/orders/approved/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/orders/draft/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/orders/executed/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/orders/rejected/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/portfolio/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/fundamental/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/instrument/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/macro/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/news/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/policy/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/portfolio/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/postmortem/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/risk/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/technical/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/valuation/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/research/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/module.json +19 -0
- workspace_templates/modules/paper-trading/files/.tradingcodex/mcp/adapters/paper-trading.py +4 -0
- workspace_templates/modules/paper-trading/module.json +16 -0
- workspace_templates/modules/postmortem/files/.tradingcodex/workflows/postmortem.yaml +12 -0
- workspace_templates/modules/postmortem/module.json +16 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/investment-workflow-map/SKILL.md +106 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/investment-workflow-map/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/manage-optional-skills/SKILL.md +101 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/manage-optional-skills/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/manage-subagents/SKILL.md +140 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/manage-subagents/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/orchestrate-workflow/SKILL.md +140 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/orchestrate-workflow/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/postmortem/SKILL.md +31 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/postmortem/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/scenario-quality-gates/SKILL.md +138 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/scenario-quality-gates/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/strategy-creator/SKILL.md +109 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/strategy-creator/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/synthesize-decision/SKILL.md +54 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/synthesize-decision/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/execution-operator/execute-paper-order/SKILL.md +35 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/execution-operator/execute-paper-order/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/fundamental-analyst/fundamental-analysis/SKILL.md +46 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/fundamental-analyst/fundamental-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/instrument-analyst/instrument-analysis/SKILL.md +40 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/instrument-analyst/instrument-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/macro-analyst/macro-analysis/SKILL.md +40 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/macro-analyst/macro-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/news-analyst/news-analysis/SKILL.md +43 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/news-analyst/news-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/create-order-intent/SKILL.md +46 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/create-order-intent/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/portfolio-review/SKILL.md +44 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/portfolio-review/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/approve-order/SKILL.md +38 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/approve-order/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/policy-review/SKILL.md +43 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/policy-review/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/review-risk/SKILL.md +45 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/review-risk/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/collect-evidence/SKILL.md +46 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/collect-evidence/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/external-data-source-gate/SKILL.md +66 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/external-data-source-gate/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/technical-analyst/technical-analysis/SKILL.md +43 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/technical-analyst/technical-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/valuation-analyst/valuation-review/SKILL.md +47 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/valuation-analyst/valuation-review/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/module.json +37 -0
- workspace_templates/modules/stub-execution/files/.tradingcodex/mcp/adapters/stub-execution.py +4 -0
- workspace_templates/modules/stub-execution/module.json +15 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/adapters/live-adapter.contract.md +25 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/enforcer/README.md +5 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/gateway/README.md +8 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/server.py +27 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/smoke-call.py +18 -0
- workspace_templates/modules/tradingcodex-mcp/module.json +24 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from tradingcodex_service.application.common import _status_class, _unique
|
|
8
|
+
from tradingcodex_service.application.agents import (
|
|
9
|
+
EXPECTED_SKILLS,
|
|
10
|
+
EXPECTED_SUBAGENTS,
|
|
11
|
+
ROLE_PERMISSION_PROFILES,
|
|
12
|
+
ROLE_SKILL_MAP,
|
|
13
|
+
USER_VISIBLE_SKILLS,
|
|
14
|
+
)
|
|
15
|
+
from tradingcodex_service.application.components import count_harness_component_tags, list_harness_components
|
|
16
|
+
from tradingcodex_service.application.policy import EXPLICIT_DENY_ACTIONS
|
|
17
|
+
from tradingcodex_service.application.research import list_workspace_research_artifacts
|
|
18
|
+
from tradingcodex_service.application.runtime import ensure_runtime_database, tradingcodex_db_path, workspace_context_payload
|
|
19
|
+
from tradingcodex_service.mcp_runtime import static_mcp_tools as _static_mcp_tools
|
|
20
|
+
|
|
21
|
+
INVESTMENT_WORKFLOW_TERMS = re.compile(
|
|
22
|
+
r"\b("
|
|
23
|
+
r"stock|stocks|equity|equities|share|shares|security|securities|ticker|tickers|"
|
|
24
|
+
r"etf|index|indices|option|options|derivative|futures|crypto|bitcoin|btc|ethereum|eth|"
|
|
25
|
+
r"bond|bonds|credit|cds|rates|fx|currency|commodity|commodities|oil|gold|"
|
|
26
|
+
r"portfolio|position|holding|exposure|sizing|hedge|risk|drawdown|"
|
|
27
|
+
r"valuation|fair value|target price|earnings|filing|catalyst|thesis|"
|
|
28
|
+
r"technical|price action|trend|momentum|volume|volatility|"
|
|
29
|
+
r"buy|sell|short|long|trade|trading|order intent|paper order|broker|"
|
|
30
|
+
r"approve|approval|execute|execution"
|
|
31
|
+
r")\b"
|
|
32
|
+
)
|
|
33
|
+
INVESTMENT_ACTION_WITH_SYMBOL = re.compile(r"\b(analyze|analyse|review|research|evaluate|assess)\b")
|
|
34
|
+
SYMBOL_LIKE_TOKEN = re.compile(r"\b[A-Z][A-Z0-9.\-]{0,6}\b")
|
|
35
|
+
NON_INVESTMENT_CONTEXT_TERMS = re.compile(
|
|
36
|
+
r"\b("
|
|
37
|
+
r"repo|repository|code|docs|documentation|readme|file|files|python|django|"
|
|
38
|
+
r"pytest|test|tests|template|templates|prompt|prompts|hook|hooks|config|"
|
|
39
|
+
r"migration|model|admin|api|mcp|cli|function|class|bug|fix|implement|"
|
|
40
|
+
r"refactor|skill|skills"
|
|
41
|
+
r")\b|agents\.md"
|
|
42
|
+
)
|
|
43
|
+
STRATEGY_AUTHORING_TERMS = re.compile(
|
|
44
|
+
r"\b(create|write|draft|define|design|build|update|edit|revise|archive|delete|activate)\b"
|
|
45
|
+
r"[^.?!]{0,100}\b(strategy|strategies|strategy skill|strategy library|entry criteria|exit criteria|sizing rule)\b|"
|
|
46
|
+
r"\b(strategy|strategies|strategy skill|strategy library)\b"
|
|
47
|
+
r"[^.?!]{0,100}\b(create|write|draft|define|design|build|update|edit|revise|archive|delete|activate)\b",
|
|
48
|
+
re.I,
|
|
49
|
+
)
|
|
50
|
+
SECRET_WARNING_TERMS = re.compile(
|
|
51
|
+
r"\b(api\s+key|apikey|broker\s+key|secret|token|credential|credentials|password)\b|\.env"
|
|
52
|
+
)
|
|
53
|
+
SECRET_ONLY_IGNORE_TERMS = re.compile(
|
|
54
|
+
r"\b("
|
|
55
|
+
r"api|key|broker|secret|token|credential|credentials|password|env|file|"
|
|
56
|
+
r"save|store|write|read|rotate|keep|put|here|is|my|to|in|the|a|an|this"
|
|
57
|
+
r")\b|\.env"
|
|
58
|
+
)
|
|
59
|
+
HANDOFF_STATES = ("accepted", "revise", "blocked", "waiting")
|
|
60
|
+
ROLE_HANDOFF_CONTRACTS: dict[str, dict[str, str]] = {
|
|
61
|
+
"head-manager": {
|
|
62
|
+
"receives": "User request, accepted role artifacts, workflow/service state.",
|
|
63
|
+
"returns": "Lane, selected team, compact briefs, accepted artifacts, conflicts, and next allowed action.",
|
|
64
|
+
"quality_gate": "Marks handoffs accepted, revise, blocked, or waiting before moving the workflow forward.",
|
|
65
|
+
"overlap_rule": "Coordinates and synthesizes; does not replace specialist role analysis.",
|
|
66
|
+
},
|
|
67
|
+
"fundamental-analyst": {
|
|
68
|
+
"receives": "Assigned evidence and source references.",
|
|
69
|
+
"returns": "Fundamental report with source/as-of posture, confidence, and missing evidence.",
|
|
70
|
+
"quality_gate": "Business, financial, filing, and economics claims stay evidence-backed and role-owned.",
|
|
71
|
+
"overlap_rule": "Does not create valuation, order intent, approval, or execution posture.",
|
|
72
|
+
},
|
|
73
|
+
"technical-analyst": {
|
|
74
|
+
"receives": "Assigned market-data references and user-stated technical constraints.",
|
|
75
|
+
"returns": "Technical report with setup observations, data posture, confidence, and invalidation gaps.",
|
|
76
|
+
"quality_gate": "Price, volume, volatility, and liquidity claims show data timing and uncertainty.",
|
|
77
|
+
"overlap_rule": "Does not turn a setup into a standalone investment conclusion or order intent.",
|
|
78
|
+
},
|
|
79
|
+
"news-analyst": {
|
|
80
|
+
"receives": "Assigned filings, disclosures, news, and source references.",
|
|
81
|
+
"returns": "Dated event report with source-quality caveats and unresolved claims.",
|
|
82
|
+
"quality_gate": "Events separate verified facts, source claims, narrative inference, and rumor risk.",
|
|
83
|
+
"overlap_rule": "Does not approve execution or assert unverified rumors as facts.",
|
|
84
|
+
},
|
|
85
|
+
"macro-analyst": {
|
|
86
|
+
"receives": "Assigned macro sources and relevant accepted role artifacts.",
|
|
87
|
+
"returns": "Macro transmission report with source/as-of posture and regime uncertainty.",
|
|
88
|
+
"quality_gate": "Transmission channels distinguish factual data, assumptions, and inference.",
|
|
89
|
+
"overlap_rule": "Does not imply futures, FX, commodity, rates, options, or live execution support.",
|
|
90
|
+
},
|
|
91
|
+
"instrument-analyst": {
|
|
92
|
+
"receives": "Assigned instrument sources, contract/methodology references, and support questions.",
|
|
93
|
+
"returns": "Instrument support report with mechanics, liquidity/support gaps, and blocked execution implications.",
|
|
94
|
+
"quality_gate": "Instrument facts, market-structure claims, and support gaps are source-backed.",
|
|
95
|
+
"overlap_rule": "Does not infer execution eligibility from data availability or router coverage.",
|
|
96
|
+
},
|
|
97
|
+
"valuation-analyst": {
|
|
98
|
+
"receives": "Accepted research artifacts and user-stated method constraints.",
|
|
99
|
+
"returns": "Valuation report with assumptions, sensitivity, confidence, and readiness for portfolio/risk review.",
|
|
100
|
+
"quality_gate": "Valuation assumptions and current-price implications are explicit and source-aware.",
|
|
101
|
+
"overlap_rule": "Does not approve, execute, or replace missing research evidence.",
|
|
102
|
+
},
|
|
103
|
+
"portfolio-manager": {
|
|
104
|
+
"receives": "Accepted research/valuation artifacts and portfolio state.",
|
|
105
|
+
"returns": "Portfolio fit report and, only when allowed, draft order readiness or draft order intent.",
|
|
106
|
+
"quality_gate": "Sizing, cash, concentration, liquidity, and opportunity-cost assumptions are visible.",
|
|
107
|
+
"overlap_rule": "Does not self-approve, execute, or repair missing research/valuation work.",
|
|
108
|
+
},
|
|
109
|
+
"risk-manager": {
|
|
110
|
+
"receives": "Accepted portfolio/order artifacts, policy state, restricted-list state, and audit evidence.",
|
|
111
|
+
"returns": "Risk/policy report, approval readiness state, approval receipt when allowed, or blocked reasons.",
|
|
112
|
+
"quality_gate": "Downside, limits, restricted-list, and approval-readiness checks are explicit.",
|
|
113
|
+
"overlap_rule": "Does not draft orders, submit execution, or loosen policy in the same workflow.",
|
|
114
|
+
},
|
|
115
|
+
"execution-operator": {
|
|
116
|
+
"receives": "Approved order intent, approval receipt, and policy allow state.",
|
|
117
|
+
"returns": "Execution result, MCP response, audit reference, or rejected/blocked reasons.",
|
|
118
|
+
"quality_gate": "Execution uses only approved paper/stub MCP paths and records audit evidence.",
|
|
119
|
+
"overlap_rule": "Does not approve, change policy, read secrets, or call raw broker APIs.",
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
EDGE_GROUP_CONTRACTS: dict[str, str] = {
|
|
123
|
+
"dispatch": "Assignment envelope only: original request, constraints, research artifact language, lane, artifact path, out-of-scope actions, and return contract.",
|
|
124
|
+
"research-handoff": "Accepted research artifacts move to valuation; weak or missing evidence returns to the owning research role.",
|
|
125
|
+
"portfolio-risk-gate": "Accepted research/valuation/instrument context moves to portfolio and risk review; gaps block draft readiness.",
|
|
126
|
+
"approval-gate": "Portfolio draft readiness moves to risk approval only after schema, policy, and role-separation checks.",
|
|
127
|
+
"execution-gate": "Risk approval moves to execution only with approved order intent, approval receipt, policy allow, idempotency, adapter, and audit.",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def is_investment_workflow_request(request: str) -> bool:
|
|
132
|
+
text = request.strip()
|
|
133
|
+
if not text:
|
|
134
|
+
return False
|
|
135
|
+
if is_secret_only_request(text):
|
|
136
|
+
return False
|
|
137
|
+
lower = text.lower()
|
|
138
|
+
if "$orchestrate-workflow" in lower:
|
|
139
|
+
return True
|
|
140
|
+
if STRATEGY_AUTHORING_TERMS.search(lower):
|
|
141
|
+
return False
|
|
142
|
+
if INVESTMENT_WORKFLOW_TERMS.search(lower):
|
|
143
|
+
return True
|
|
144
|
+
if INVESTMENT_ACTION_WITH_SYMBOL.search(lower) and SYMBOL_LIKE_TOKEN.search(text):
|
|
145
|
+
if NON_INVESTMENT_CONTEXT_TERMS.search(lower):
|
|
146
|
+
return False
|
|
147
|
+
return True
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def is_secret_warning_request(request: str) -> bool:
|
|
152
|
+
return bool(SECRET_WARNING_TERMS.search(request.lower()))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def is_secret_only_request(request: str) -> bool:
|
|
156
|
+
if not is_secret_warning_request(request):
|
|
157
|
+
return False
|
|
158
|
+
reduced = SECRET_ONLY_IGNORE_TERMS.sub(" ", request.lower())
|
|
159
|
+
if INVESTMENT_WORKFLOW_TERMS.search(reduced):
|
|
160
|
+
return False
|
|
161
|
+
return not (INVESTMENT_ACTION_WITH_SYMBOL.search(reduced) and SYMBOL_LIKE_TOKEN.search(request))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def classify_starter_request(request: str) -> dict[str, Any]:
|
|
165
|
+
text = request.lower()
|
|
166
|
+
universe = classify_investment_universe(text)
|
|
167
|
+
action_text = strip_guardrail_verification_phrases(strip_negated_action_phrases(text))
|
|
168
|
+
valuation_blocked = negates_scope(text, r"valuation|fair value|target price|price target|multiples|dcf")
|
|
169
|
+
technical_blocked = negates_scope(text, r"technical(?: analysis)?|chart|price action")
|
|
170
|
+
news_blocked = negates_scope(text, r"news(?: analysis)?|headline|event review")
|
|
171
|
+
wants_approval_execution = bool(re.search(r"submit|already approved|approved paper|execute|execution|approve|approval|broker|live", action_text))
|
|
172
|
+
wants_order_draft = bool(re.search(r"draft|order intent|buy order|sell order|paper buy order|paper sell order", action_text))
|
|
173
|
+
wants_decision = bool(re.search(r"should i buy|should i sell|recommend|fair value|target price|buy|sell", action_text))
|
|
174
|
+
wants_thesis_review = bool(re.search(r"earnings|filing|catalyst|preview|thesis|valuation|disclosure|narrative", action_text))
|
|
175
|
+
wants_portfolio_risk = bool(re.search(r"portfolio|position|holding|own|exposure|concentration|correlation|drawdown|hedge|sizing|size|risk", action_text))
|
|
176
|
+
wants_macro = bool(re.search(r"macro|rates|rate|fx|currency|commodity|commodities|inflation|fed|boj|ecb|central bank|yield|oil|gold", action_text))
|
|
177
|
+
wants_instrument = bool(
|
|
178
|
+
re.search(
|
|
179
|
+
r"\b(etf|index|indices|option|options|derivative|futures|borrow|crypto|bitcoin|btc|ethereum|eth|cds|bond|credit|convertible|preferred|instrument)\b|"
|
|
180
|
+
r"\b(short interest|market structure)\b",
|
|
181
|
+
action_text,
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
wants_technical = bool(re.search(r"trend|technical|price|volatility|liquidity|drawdown|down|setup|chart", action_text))
|
|
185
|
+
wants_news = bool(re.search(r"news|event|earnings|filing|headline|catalyst|disclosure", action_text))
|
|
186
|
+
research = base_research_team(universe, wants_technical, wants_news)
|
|
187
|
+
if technical_blocked:
|
|
188
|
+
research = [role for role in research if role != "technical-analyst"]
|
|
189
|
+
if news_blocked:
|
|
190
|
+
research = [role for role in research if role != "news-analyst"]
|
|
191
|
+
if wants_macro:
|
|
192
|
+
research.append("macro-analyst")
|
|
193
|
+
if wants_instrument:
|
|
194
|
+
research.append("instrument-analyst")
|
|
195
|
+
thesis_roles = research + ([] if valuation_blocked else ["valuation-analyst"])
|
|
196
|
+
if wants_approval_execution:
|
|
197
|
+
return {"universe": universe, "lane": "order_intent_or_approval_execution_gate", "subagents": _unique((["macro-analyst"] if wants_macro else []) + (["instrument-analyst"] if wants_instrument else []) + ["portfolio-manager", "risk-manager", "execution-operator"]), "blockedActions": ["natural-language order", "direct broker API", "secret read", "execution without approved artifacts"]}
|
|
198
|
+
if wants_order_draft:
|
|
199
|
+
return {"universe": universe, "lane": "order_intent_draft_gate", "subagents": _unique(research + ["portfolio-manager", "risk-manager"]), "blockedActions": ["approval", "execution", "direct broker API", "secret read"]}
|
|
200
|
+
if wants_decision:
|
|
201
|
+
return {"universe": universe, "lane": "thesis_review_then_portfolio_risk_review", "subagents": _unique(thesis_roles + ["portfolio-manager", "risk-manager"]), "blockedActions": ["order intent", "approval", "execution", "direct broker API", "secret read"]}
|
|
202
|
+
if wants_portfolio_risk:
|
|
203
|
+
return {"universe": universe, "lane": "portfolio_risk_review", "subagents": _unique((["macro-analyst"] if wants_macro else []) + (["instrument-analyst"] if wants_instrument else []) + (["technical-analyst"] if wants_technical else []) + (["news-analyst"] if wants_news else []) + ["portfolio-manager", "risk-manager"]), "blockedActions": ["order intent", "approval", "execution", "direct broker API", "secret read"]}
|
|
204
|
+
if wants_thesis_review and universe == "public_equity":
|
|
205
|
+
return {"universe": universe, "lane": "thesis_review", "subagents": _unique(thesis_roles), "blockedActions": ["order intent", "approval", "execution", "direct broker API", "secret read"]}
|
|
206
|
+
return {"universe": universe, "lane": "research_only", "subagents": _unique(research), "blockedActions": ["valuation unless requested", "order intent", "approval", "execution", "direct broker API", "secret read"]}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def classify_investment_universe(text: str) -> str:
|
|
210
|
+
if re.search(r"\b(btc|bitcoin|eth|ethereum|crypto|token|stablecoin|on-chain|defi)\b", text):
|
|
211
|
+
return "public_crypto"
|
|
212
|
+
if re.search(r"\b(option|options|derivative|futures|swap|volatility surface)\b", text):
|
|
213
|
+
return "options_derivatives"
|
|
214
|
+
if re.search(r"\b(etf|index|indices|benchmark|constituent)\b", text):
|
|
215
|
+
return "etf_index"
|
|
216
|
+
if re.search(r"\b(cds|bond|credit|spread|covenant|restructuring|distressed|loan)\b", text):
|
|
217
|
+
return "credit_signal"
|
|
218
|
+
if re.search(r"\b(macro|rates|fx|currency|commodity|commodities|inflation|fed|boj|ecb|central bank|yield|oil|gold)\b", text):
|
|
219
|
+
return "macro_rates_fx_commodities"
|
|
220
|
+
return "public_equity"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def base_research_team(universe: str, wants_technical: bool, wants_news: bool) -> list[str]:
|
|
224
|
+
if universe == "public_crypto":
|
|
225
|
+
return ["technical-analyst", "news-analyst", "instrument-analyst"]
|
|
226
|
+
if universe == "macro_rates_fx_commodities":
|
|
227
|
+
team = ["macro-analyst"]
|
|
228
|
+
if wants_technical:
|
|
229
|
+
team.append("technical-analyst")
|
|
230
|
+
if wants_news:
|
|
231
|
+
team.append("news-analyst")
|
|
232
|
+
return team
|
|
233
|
+
if universe in {"options_derivatives", "credit_signal"}:
|
|
234
|
+
team = ["instrument-analyst"]
|
|
235
|
+
if wants_technical:
|
|
236
|
+
team.append("technical-analyst")
|
|
237
|
+
if wants_news:
|
|
238
|
+
team.append("news-analyst")
|
|
239
|
+
return team
|
|
240
|
+
if universe == "etf_index":
|
|
241
|
+
return ["instrument-analyst", "technical-analyst", "news-analyst"]
|
|
242
|
+
return ["fundamental-analyst", "technical-analyst", "news-analyst"]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def build_subagent_starter_prompt(request: str) -> str:
|
|
246
|
+
plan = classify_starter_request(request)
|
|
247
|
+
artifact_language = infer_research_artifact_language(request)
|
|
248
|
+
return "\n".join([
|
|
249
|
+
"Use this workspace's fixed-role subagent workflow.",
|
|
250
|
+
"Explicitly use Codex subagents.",
|
|
251
|
+
f'Original user request (verbatim): "{request}"',
|
|
252
|
+
f"Research artifact language: {artifact_language}",
|
|
253
|
+
f"Investment universe: {plan['universe']}",
|
|
254
|
+
f"Workflow lane: {plan['lane']}",
|
|
255
|
+
f"Spawn these fixed role subagents in parallel: {', '.join(plan['subagents'])}",
|
|
256
|
+
"This selected team is binding for the current lane; do not spawn roles outside this exact list unless the user later asks for a broader lane.",
|
|
257
|
+
"For `research_only`, do not add valuation, portfolio, risk, approval, or execution roles.",
|
|
258
|
+
"When calling `spawn_agent` for a fixed role, use `agent_type` and a compact `message`; do not set `fork_context` to true.",
|
|
259
|
+
"Use each role's exact `.codex/agents/*.toml` name as the runtime label.",
|
|
260
|
+
"Preserve the original user request and explicit constraints in every subagent brief.",
|
|
261
|
+
"Tell each subagent to write reader-facing research artifacts in the research artifact language unless the user explicitly requested a different artifact language.",
|
|
262
|
+
"Do not let head-manager perform substantive investment analysis before subagent outputs exist.",
|
|
263
|
+
"Require each role handoff to include artifact path, handoff state, source/as-of posture, confidence, missing evidence, readiness/support gaps, next eligible recipient, and blocked actions.",
|
|
264
|
+
"Use handoff states: accepted, revise, blocked, waiting.",
|
|
265
|
+
"Do not let downstream roles redo missing upstream work; request revision from the owning role or stop with waiting/blocked status.",
|
|
266
|
+
"Wait for all selected subagents, then synthesize their outputs with artifact paths, handoff states, disagreements, missing evidence, and next allowed action.",
|
|
267
|
+
f"Blocked actions before artifacts: {', '.join(plan['blockedActions'])}",
|
|
268
|
+
])
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def infer_research_artifact_language(request: str) -> str:
|
|
272
|
+
text = request.strip()
|
|
273
|
+
lower = text.lower()
|
|
274
|
+
if not text:
|
|
275
|
+
return "same language as the original user request unless explicitly overridden"
|
|
276
|
+
explicit_language_patterns = [
|
|
277
|
+
(r"\bkorean\b|한국어|한글", "Korean"),
|
|
278
|
+
(r"\benglish\b|영어", "English"),
|
|
279
|
+
(r"\bjapanese\b|일본어|日本語", "Japanese"),
|
|
280
|
+
(r"\bchinese\b|중국어|中文|汉语|漢語", "Chinese"),
|
|
281
|
+
]
|
|
282
|
+
for pattern, language in explicit_language_patterns:
|
|
283
|
+
if re.search(pattern, lower):
|
|
284
|
+
return f"{language} (explicitly requested or named by the user)"
|
|
285
|
+
if re.search(r"[\uac00-\ud7a3]", text):
|
|
286
|
+
return "Korean (inferred from the original user request)"
|
|
287
|
+
if re.search(r"[\u3040-\u30ff]", text):
|
|
288
|
+
return "Japanese (inferred from the original user request)"
|
|
289
|
+
if re.search(r"[\u4e00-\u9fff]", text):
|
|
290
|
+
return "Chinese (inferred from the original user request)"
|
|
291
|
+
return "same language as the original user request unless explicitly overridden"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def strip_negated_action_phrases(text: str) -> str:
|
|
295
|
+
text = re.sub(r"\b(no|do not|don't|dont|without)\s+(?:a\s+|any\s+)?(account access|order draft|trade execution|trading|trade|trades|orders|order|draft|execution|execute|approval|approve|buy|sell|recommendation|recommend|decision support|valuation|fair value|target price|price target|multiples|dcf|technical analysis|technical|chart|price action|news analysis|news|headline|event review|portfolio review|portfolio|risk review|risk)\b", " ", text)
|
|
296
|
+
text = re.sub(r"\b(no|do not|don't|dont|without)\s+(live trading|live execution|broker access|account|broker|trade execution)\b", " ", text)
|
|
297
|
+
text = re.sub(r"\bnot\s+(?:asking\s+for\s+|requesting\s+|seeking\s+)?(?:a\s+|any\s+)?(order|trade|execution|approval|valuation|fair value|target price|price target|recommendation|portfolio review|risk review|technical analysis|news analysis)\b", " ", text)
|
|
298
|
+
return text
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def strip_guardrail_verification_phrases(text: str) -> str:
|
|
302
|
+
text = re.sub(r"\beven with\b[^.]{0,180}\b(?:blocked|denied|rejected|unavailable)[-\s]+action\s+wording\b[^.]*", " ", text)
|
|
303
|
+
text = re.sub(r"\b(?:blocked|denied|rejected|unavailable)[-\s]+action\s+wording\s+like\b[^.]*", " ", text)
|
|
304
|
+
text = re.sub(r"\bwhether\s+(?:order|approval|execution|direct|broker|secret|access|/|\s|was|were|is|are|blocked|denied|rejected)+", " ", text)
|
|
305
|
+
text = re.sub(r"\b(?:blocked|denied|rejected|unavailable)\s+(?:order|approval|execution|direct|broker|secret|access|/|\s|actions|paths)+", " ", text)
|
|
306
|
+
text = re.sub(r"\bverify\s+(?:routing\s+and\s+)?(?:blocked|denied|rejected|unavailable)\s+(?:actions|paths|access)?", " ", text)
|
|
307
|
+
text = re.sub(r"\bverify\b.{0,120}\b(?:blocked|denied|rejected|unavailable|no trading|no order|no execution)\b", " ", text)
|
|
308
|
+
text = re.sub(r"\bconfirm\b.{0,120}\b(?:blocked|denied|rejected|unavailable|no trading|no order|no execution)\b", " ", text)
|
|
309
|
+
return text
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def negates_scope(text: str, scope_pattern: str) -> bool:
|
|
313
|
+
return bool(re.search(rf"\b(?:no|do not|don't|dont|without)\s+(?:a\s+|any\s+)?(?:{scope_pattern})\b", text)) or bool(
|
|
314
|
+
re.search(rf"\bnot\s+(?:asking\s+for\s+|requesting\s+|seeking\s+)?(?:a\s+|any\s+)?(?:{scope_pattern})\b", text)
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
ROLE_UI_PROFILES: dict[str, dict[str, Any]] = {
|
|
319
|
+
"head-manager": {
|
|
320
|
+
"label": "Head Manager",
|
|
321
|
+
"group": "main",
|
|
322
|
+
"purpose": "Routes the request, coordinates fixed subagents, waits for artifacts, and synthesizes the workflow state.",
|
|
323
|
+
"forbidden_actions": [
|
|
324
|
+
"Do not replace specialist analysis with direct analysis.",
|
|
325
|
+
"Do not call broker APIs directly.",
|
|
326
|
+
"Do not bypass policy, approval, adapter, or audit checks.",
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
"fundamental-analyst": {
|
|
330
|
+
"label": "Fundamental Analyst",
|
|
331
|
+
"group": "research",
|
|
332
|
+
"purpose": "Reviews business quality, financial statements, official disclosures, and competitive position.",
|
|
333
|
+
"forbidden_actions": ["No order intent.", "No approval.", "No execution.", "No secret access."],
|
|
334
|
+
},
|
|
335
|
+
"technical-analyst": {
|
|
336
|
+
"label": "Technical Analyst",
|
|
337
|
+
"group": "research",
|
|
338
|
+
"purpose": "Reviews price action, trend, volatility, liquidity, volume, and market setup.",
|
|
339
|
+
"forbidden_actions": ["No order intent.", "No execution.", "No standalone investment conclusion."],
|
|
340
|
+
},
|
|
341
|
+
"news-analyst": {
|
|
342
|
+
"label": "News Analyst",
|
|
343
|
+
"group": "research",
|
|
344
|
+
"purpose": "Reviews news, official disclosures, event risk, catalysts, and narrative change.",
|
|
345
|
+
"forbidden_actions": ["No unverified rumor claims.", "No execution.", "No secret access."],
|
|
346
|
+
},
|
|
347
|
+
"macro-analyst": {
|
|
348
|
+
"label": "Macro Analyst",
|
|
349
|
+
"group": "research",
|
|
350
|
+
"purpose": "Reviews rates, FX, commodities, liquidity, policy, and cross-asset transmission.",
|
|
351
|
+
"forbidden_actions": ["No order intent.", "No execution.", "No unsupported implementation claims."],
|
|
352
|
+
},
|
|
353
|
+
"instrument-analyst": {
|
|
354
|
+
"label": "Instrument Analyst",
|
|
355
|
+
"group": "research",
|
|
356
|
+
"purpose": "Reviews ETF/index, options, derivatives, crypto public markets, credit signals, and instrument mechanics.",
|
|
357
|
+
"forbidden_actions": ["No order intent.", "No execution.", "No unsupported instrument execution claims."],
|
|
358
|
+
},
|
|
359
|
+
"valuation-analyst": {
|
|
360
|
+
"label": "Valuation Analyst",
|
|
361
|
+
"group": "analysis",
|
|
362
|
+
"purpose": "Builds valuation, scenario, multiple, DCF, reverse DCF, and expected-return views.",
|
|
363
|
+
"forbidden_actions": ["No approval.", "No execution.", "No broker API calls."],
|
|
364
|
+
},
|
|
365
|
+
"portfolio-manager": {
|
|
366
|
+
"label": "Portfolio Manager",
|
|
367
|
+
"group": "portfolio",
|
|
368
|
+
"purpose": "Reviews portfolio fit, sizing, cash, concentration, and draft order intent readiness.",
|
|
369
|
+
"forbidden_actions": ["No self-approval.", "No execution.", "No arbitrary policy changes."],
|
|
370
|
+
},
|
|
371
|
+
"risk-manager": {
|
|
372
|
+
"label": "Risk Manager",
|
|
373
|
+
"group": "risk",
|
|
374
|
+
"purpose": "Reviews risk, restricted list, downside, policy readiness, and approval receipt eligibility.",
|
|
375
|
+
"forbidden_actions": ["No order drafting.", "No execution.", "No arbitrary policy changes."],
|
|
376
|
+
},
|
|
377
|
+
"execution-operator": {
|
|
378
|
+
"label": "Execution Operator",
|
|
379
|
+
"group": "execution",
|
|
380
|
+
"purpose": "Submits approved order intents through TradingCodex MCP using paper or stub adapters only.",
|
|
381
|
+
"forbidden_actions": ["No raw broker API.", "No secret read.", "No policy change.", "No live broker path in core."],
|
|
382
|
+
},
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
ROLE_NODE_POSITIONS: dict[str, tuple[int, int]] = {
|
|
387
|
+
"head-manager": (50, 10),
|
|
388
|
+
"fundamental-analyst": (12, 29),
|
|
389
|
+
"technical-analyst": (31, 29),
|
|
390
|
+
"news-analyst": (50, 29),
|
|
391
|
+
"macro-analyst": (69, 29),
|
|
392
|
+
"instrument-analyst": (88, 29),
|
|
393
|
+
"valuation-analyst": (31, 53),
|
|
394
|
+
"portfolio-manager": (50, 66),
|
|
395
|
+
"risk-manager": (69, 78),
|
|
396
|
+
"execution-operator": (88, 91),
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
TOPOLOGY_EDGES: tuple[dict[str, str], ...] = (
|
|
401
|
+
{"source": "head-manager", "target": "fundamental-analyst", "group": "dispatch"},
|
|
402
|
+
{"source": "head-manager", "target": "technical-analyst", "group": "dispatch"},
|
|
403
|
+
{"source": "head-manager", "target": "news-analyst", "group": "dispatch"},
|
|
404
|
+
{"source": "head-manager", "target": "macro-analyst", "group": "dispatch"},
|
|
405
|
+
{"source": "head-manager", "target": "instrument-analyst", "group": "dispatch"},
|
|
406
|
+
{"source": "fundamental-analyst", "target": "valuation-analyst", "group": "research-handoff"},
|
|
407
|
+
{"source": "technical-analyst", "target": "valuation-analyst", "group": "research-handoff"},
|
|
408
|
+
{"source": "news-analyst", "target": "valuation-analyst", "group": "research-handoff"},
|
|
409
|
+
{"source": "macro-analyst", "target": "portfolio-manager", "group": "portfolio-risk-gate"},
|
|
410
|
+
{"source": "instrument-analyst", "target": "portfolio-manager", "group": "portfolio-risk-gate"},
|
|
411
|
+
{"source": "valuation-analyst", "target": "portfolio-manager", "group": "portfolio-risk-gate"},
|
|
412
|
+
{"source": "portfolio-manager", "target": "risk-manager", "group": "approval-gate"},
|
|
413
|
+
{"source": "risk-manager", "target": "execution-operator", "group": "execution-gate"},
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
EDGE_GROUP_LABELS: dict[str, str] = {
|
|
418
|
+
"dispatch": "Dispatch",
|
|
419
|
+
"research-handoff": "Research handoff",
|
|
420
|
+
"portfolio-risk-gate": "Portfolio/risk gate",
|
|
421
|
+
"approval-gate": "Approval gate",
|
|
422
|
+
"execution-gate": "Execution gate",
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def get_harness_topology(workspace_root: Path | str | None = None) -> dict[str, Any]:
|
|
427
|
+
tools = _static_mcp_tools()
|
|
428
|
+
nodes = []
|
|
429
|
+
for role, skills in ROLE_SKILL_MAP.items():
|
|
430
|
+
x, y = ROLE_NODE_POSITIONS[role]
|
|
431
|
+
profile = ROLE_UI_PROFILES[role]
|
|
432
|
+
allowed_tools = _allowed_tools_for_role(role, tools)
|
|
433
|
+
nodes.append({
|
|
434
|
+
"role": role,
|
|
435
|
+
"label": profile["label"],
|
|
436
|
+
"group": profile["group"],
|
|
437
|
+
"purpose": profile["purpose"],
|
|
438
|
+
"skills_count": len(skills),
|
|
439
|
+
"tools_count": len(allowed_tools),
|
|
440
|
+
"x": x,
|
|
441
|
+
"y": y,
|
|
442
|
+
})
|
|
443
|
+
edges = []
|
|
444
|
+
for edge in TOPOLOGY_EDGES:
|
|
445
|
+
source_x, source_y = ROLE_NODE_POSITIONS[edge["source"]]
|
|
446
|
+
target_x, target_y = ROLE_NODE_POSITIONS[edge["target"]]
|
|
447
|
+
mid_y = round((source_y + target_y) / 2, 2)
|
|
448
|
+
edges.append({
|
|
449
|
+
**edge,
|
|
450
|
+
"label": EDGE_GROUP_LABELS[edge["group"]],
|
|
451
|
+
"contract": EDGE_GROUP_CONTRACTS[edge["group"]],
|
|
452
|
+
"source_x": source_x,
|
|
453
|
+
"source_y": source_y,
|
|
454
|
+
"target_x": target_x,
|
|
455
|
+
"target_y": target_y,
|
|
456
|
+
"mid_y": mid_y,
|
|
457
|
+
})
|
|
458
|
+
return {
|
|
459
|
+
"nodes": nodes,
|
|
460
|
+
"edges": edges,
|
|
461
|
+
"edge_groups": [{"key": key, "label": label, "contract": EDGE_GROUP_CONTRACTS[key]} for key, label in EDGE_GROUP_LABELS.items()],
|
|
462
|
+
"handoff_states": list(HANDOFF_STATES),
|
|
463
|
+
"systems": get_harness_systems(),
|
|
464
|
+
"components": list_harness_components(),
|
|
465
|
+
"component_tag_counts": count_harness_component_tags(),
|
|
466
|
+
"layers": [
|
|
467
|
+
{"label": "Coordinator", "y": 10},
|
|
468
|
+
{"label": "Research roles", "y": 29},
|
|
469
|
+
{"label": "Valuation", "y": 53},
|
|
470
|
+
{"label": "Portfolio fit", "y": 66},
|
|
471
|
+
{"label": "Risk approval", "y": 78},
|
|
472
|
+
{"label": "MCP execution", "y": 91},
|
|
473
|
+
],
|
|
474
|
+
"boundary": {
|
|
475
|
+
"label": "MCP execution boundary",
|
|
476
|
+
"summary": "Execution-sensitive actions must pass principal, capability, policy, schema, approval, idempotency, adapter, and audit checks.",
|
|
477
|
+
"x": 78,
|
|
478
|
+
"y1": 72,
|
|
479
|
+
"y2": 96,
|
|
480
|
+
},
|
|
481
|
+
"db_canonical": True,
|
|
482
|
+
"workspace_context": workspace_context_payload(workspace_root),
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def get_harness_systems() -> list[dict[str, Any]]:
|
|
487
|
+
return [
|
|
488
|
+
{
|
|
489
|
+
"key": "guardrails",
|
|
490
|
+
"label": "Guardrails",
|
|
491
|
+
"summary": "Reduce, isolate, and block risky behavior before executable action.",
|
|
492
|
+
"items": [
|
|
493
|
+
{"label": "Guidance", "summary": "Prompts, skills, hooks, and checklists shape agent behavior."},
|
|
494
|
+
{"label": "Enforcement", "summary": "Policy, schemas, approvals, allowlists, idempotency, and adapters block unsafe final paths."},
|
|
495
|
+
{"label": "Information barriers", "summary": "Role-local context, file walls, secret walls, and tool boundaries limit knowledge flow."},
|
|
496
|
+
],
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
"key": "improvement",
|
|
500
|
+
"label": "Improvement",
|
|
501
|
+
"summary": "Raise workflow quality and feed lessons back into the harness.",
|
|
502
|
+
"items": [
|
|
503
|
+
{"label": "Workflow quality", "summary": "Workflow maps, no-overlap handoffs, role briefs, quality gates, and readiness labels."},
|
|
504
|
+
{"label": "Research memory", "summary": "Workspace markdown artifacts, versions, source snapshots, and freshness warnings."},
|
|
505
|
+
{"label": "Skill evolution", "summary": "File proposal, validation, projection, and manifest state."},
|
|
506
|
+
{"label": "Postmortems", "summary": "Rejected orders, thesis changes, and process failures become concrete improvements."},
|
|
507
|
+
{"label": "Validation feedback", "summary": "Recurring issues become tests, smoke checks, and routing scenarios."},
|
|
508
|
+
],
|
|
509
|
+
},
|
|
510
|
+
]
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def get_role_detail(role: str, workspace_root: Path | str | None = None) -> dict[str, Any]:
|
|
514
|
+
if role not in ROLE_SKILL_MAP:
|
|
515
|
+
role = "head-manager"
|
|
516
|
+
tools = _static_mcp_tools()
|
|
517
|
+
profile = ROLE_UI_PROFILES[role]
|
|
518
|
+
return {
|
|
519
|
+
"role": role,
|
|
520
|
+
"label": profile["label"],
|
|
521
|
+
"group": profile["group"],
|
|
522
|
+
"purpose": profile["purpose"],
|
|
523
|
+
"skills": ROLE_SKILL_MAP[role],
|
|
524
|
+
"handoff_contract": ROLE_HANDOFF_CONTRACTS[role],
|
|
525
|
+
"handoff_states": list(HANDOFF_STATES),
|
|
526
|
+
"allowed_tools": _allowed_tools_for_role(role, tools),
|
|
527
|
+
"forbidden_actions": profile["forbidden_actions"],
|
|
528
|
+
"latest_artifacts": _latest_role_artifacts(role, workspace_root),
|
|
529
|
+
"latest_activity": _latest_role_activity(role),
|
|
530
|
+
"db_canonical": True,
|
|
531
|
+
"workspace_context": workspace_context_payload(workspace_root),
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def get_harness_health(workspace_root: Path | str | None = None) -> dict[str, Any]:
|
|
536
|
+
try:
|
|
537
|
+
ensure_runtime_database(workspace_root)
|
|
538
|
+
except Exception:
|
|
539
|
+
pass
|
|
540
|
+
|
|
541
|
+
from tradingcodex_service.mcp_runtime import static_mcp_tools
|
|
542
|
+
|
|
543
|
+
tools = static_mcp_tools()
|
|
544
|
+
counts = {
|
|
545
|
+
"roster": len(EXPECTED_SUBAGENTS),
|
|
546
|
+
"roles_total": len(ROLE_SKILL_MAP),
|
|
547
|
+
"skills": len(EXPECTED_SKILLS),
|
|
548
|
+
"components": len(list_harness_components()),
|
|
549
|
+
"mcp_tools": len(tools),
|
|
550
|
+
"mcp_execution_tools": sum(1 for tool in tools if tool.get("annotations", {}).get("risk_level") == "execution"),
|
|
551
|
+
"policy_blocks": _model_count("apps.policy.models", "PolicyDecision", decision="deny"),
|
|
552
|
+
"restricted_symbols": _model_count("apps.policy.models", "RestrictedSymbol", active=True),
|
|
553
|
+
"workspace_contexts": _model_count("apps.harness.models", "WorkspaceContext"),
|
|
554
|
+
"research_artifacts": _workspace_research_count(workspace_root),
|
|
555
|
+
"order_intents": _model_count("apps.orders.models", "OrderIntent"),
|
|
556
|
+
"approval_receipts": _model_count("apps.orders.models", "ApprovalReceipt"),
|
|
557
|
+
"execution_results": _model_count("apps.orders.models", "ExecutionResult"),
|
|
558
|
+
"mcp_calls": _model_count("apps.mcp.models", "McpToolCall"),
|
|
559
|
+
}
|
|
560
|
+
checks = [
|
|
561
|
+
{"label": "Fixed subagent roster", "value": f"{counts['roster']} of 9", "status": "good"},
|
|
562
|
+
{"label": "Repo skills installed", "value": str(counts["skills"]), "status": "good"},
|
|
563
|
+
{"label": "Handoff contract", "value": "/".join(HANDOFF_STATES), "status": "good"},
|
|
564
|
+
{"label": "Harness components", "value": str(counts["components"]), "status": "good"},
|
|
565
|
+
{"label": "MCP tools visible", "value": str(counts["mcp_tools"]), "status": "good"},
|
|
566
|
+
{"label": "Execution tools", "value": str(counts["mcp_execution_tools"]), "status": "warn"},
|
|
567
|
+
{"label": "Policy blocks", "value": str(counts["policy_blocks"]), "status": "neutral"},
|
|
568
|
+
{"label": "Workspace contexts", "value": str(counts["workspace_contexts"]), "status": "neutral"},
|
|
569
|
+
]
|
|
570
|
+
return {
|
|
571
|
+
"counts": counts,
|
|
572
|
+
"checks": checks,
|
|
573
|
+
"systems": get_harness_systems(),
|
|
574
|
+
"components": list_harness_components(),
|
|
575
|
+
"component_tag_counts": count_harness_component_tags(),
|
|
576
|
+
"db_path": str(tradingcodex_db_path()),
|
|
577
|
+
"central_local_service": True,
|
|
578
|
+
"db_canonical": True,
|
|
579
|
+
"workspace_context": workspace_context_payload(workspace_root),
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def list_recent_activity(workspace_root: Path | str | None = None, limit: int = 12) -> list[dict[str, Any]]:
|
|
584
|
+
try:
|
|
585
|
+
ensure_runtime_database(workspace_root)
|
|
586
|
+
except Exception:
|
|
587
|
+
pass
|
|
588
|
+
items: list[dict[str, Any]] = []
|
|
589
|
+
try:
|
|
590
|
+
from apps.mcp.models import McpToolCall
|
|
591
|
+
|
|
592
|
+
for call in McpToolCall.objects.order_by("-created_at", "-id")[:limit]:
|
|
593
|
+
items.append({
|
|
594
|
+
"kind": "MCP",
|
|
595
|
+
"title": call.tool_name,
|
|
596
|
+
"subtitle": call.principal_id,
|
|
597
|
+
"status": call.status,
|
|
598
|
+
"status_class": _status_class(call.status),
|
|
599
|
+
"created_at": call.created_at,
|
|
600
|
+
})
|
|
601
|
+
except Exception:
|
|
602
|
+
pass
|
|
603
|
+
try:
|
|
604
|
+
from apps.audit.models import AuditEvent
|
|
605
|
+
|
|
606
|
+
for event in AuditEvent.objects.order_by("-created_at", "-id")[:limit]:
|
|
607
|
+
items.append({
|
|
608
|
+
"kind": "Audit",
|
|
609
|
+
"title": event.action,
|
|
610
|
+
"subtitle": event.actor_principal,
|
|
611
|
+
"status": event.decision,
|
|
612
|
+
"status_class": _status_class(event.decision),
|
|
613
|
+
"created_at": event.created_at,
|
|
614
|
+
})
|
|
615
|
+
except Exception:
|
|
616
|
+
pass
|
|
617
|
+
try:
|
|
618
|
+
from apps.workflows.models import WorkflowRun
|
|
619
|
+
|
|
620
|
+
for run in WorkflowRun.objects.order_by("-created_at", "-id")[:limit]:
|
|
621
|
+
items.append({
|
|
622
|
+
"kind": "Workflow",
|
|
623
|
+
"title": run.lane,
|
|
624
|
+
"subtitle": run.universe,
|
|
625
|
+
"status": run.status,
|
|
626
|
+
"status_class": _status_class(run.status),
|
|
627
|
+
"created_at": run.created_at,
|
|
628
|
+
})
|
|
629
|
+
except Exception:
|
|
630
|
+
pass
|
|
631
|
+
items.sort(key=lambda item: item["created_at"], reverse=True)
|
|
632
|
+
return items[:limit]
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def list_policy_overview(workspace_root: Path | str | None = None) -> dict[str, Any]:
|
|
636
|
+
try:
|
|
637
|
+
ensure_runtime_database(workspace_root)
|
|
638
|
+
except Exception:
|
|
639
|
+
pass
|
|
640
|
+
restricted_symbols: list[dict[str, Any]] = []
|
|
641
|
+
decisions: list[dict[str, Any]] = []
|
|
642
|
+
principals: list[dict[str, Any]] = []
|
|
643
|
+
try:
|
|
644
|
+
from apps.policy.models import PolicyDecision, Principal, RestrictedSymbol
|
|
645
|
+
|
|
646
|
+
restricted_symbols = [
|
|
647
|
+
{"symbol": item.symbol, "reason": item.reason, "active": item.active, "status_class": "bad" if item.active else "neutral"}
|
|
648
|
+
for item in RestrictedSymbol.objects.order_by("symbol")[:50]
|
|
649
|
+
]
|
|
650
|
+
decisions = [
|
|
651
|
+
{
|
|
652
|
+
"principal_id": item.principal_id,
|
|
653
|
+
"action": item.action,
|
|
654
|
+
"resource": item.resource,
|
|
655
|
+
"decision": item.decision,
|
|
656
|
+
"reasons": item.reasons,
|
|
657
|
+
"created_at": item.created_at,
|
|
658
|
+
"status_class": _status_class(item.decision),
|
|
659
|
+
}
|
|
660
|
+
for item in PolicyDecision.objects.order_by("-created_at", "-id")[:20]
|
|
661
|
+
]
|
|
662
|
+
principals = [
|
|
663
|
+
{"principal_id": item.principal_id, "role": item.role, "active": item.active}
|
|
664
|
+
for item in Principal.objects.order_by("role", "principal_id")[:50]
|
|
665
|
+
]
|
|
666
|
+
except Exception:
|
|
667
|
+
pass
|
|
668
|
+
return {
|
|
669
|
+
"restricted_symbols": restricted_symbols,
|
|
670
|
+
"recent_decisions": decisions,
|
|
671
|
+
"principals": principals,
|
|
672
|
+
"explicit_denies": sorted(EXPLICIT_DENY_ACTIONS),
|
|
673
|
+
"db_canonical": True,
|
|
674
|
+
"workspace_context": workspace_context_payload(workspace_root),
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _allowed_tools_for_role(role: str, tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
679
|
+
allowed = []
|
|
680
|
+
for tool in tools:
|
|
681
|
+
annotations = tool.get("annotations") or {}
|
|
682
|
+
if role in annotations.get("allowed_roles", []):
|
|
683
|
+
allowed.append({
|
|
684
|
+
"name": tool["name"],
|
|
685
|
+
"category": annotations.get("category", ""),
|
|
686
|
+
"risk_level": annotations.get("risk_level", "read"),
|
|
687
|
+
"requires_approval": bool(annotations.get("requires_approval")),
|
|
688
|
+
"status_class": _status_class(annotations.get("risk_level", "read")),
|
|
689
|
+
})
|
|
690
|
+
return allowed
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _latest_role_artifacts(role: str, workspace_root: Path | str | None) -> list[dict[str, Any]]:
|
|
694
|
+
try:
|
|
695
|
+
role_alias = role.replace("-analyst", "").replace("-manager", "").replace("-operator", "")
|
|
696
|
+
artifacts = [
|
|
697
|
+
artifact
|
|
698
|
+
for artifact in list_workspace_research_artifacts(Path(workspace_root or Path.cwd()))
|
|
699
|
+
if artifact.get("created_by") == role or artifact.get("role") in {role, role_alias}
|
|
700
|
+
]
|
|
701
|
+
return [
|
|
702
|
+
{
|
|
703
|
+
"artifact_id": artifact["artifact_id"],
|
|
704
|
+
"title": artifact["title"],
|
|
705
|
+
"artifact_type": artifact["artifact_type"],
|
|
706
|
+
"universe": artifact["universe"],
|
|
707
|
+
"readiness_label": artifact.get("readiness_label") or "unlabeled",
|
|
708
|
+
"updated_at": artifact["updated_at"],
|
|
709
|
+
}
|
|
710
|
+
for artifact in artifacts[:5]
|
|
711
|
+
]
|
|
712
|
+
except Exception:
|
|
713
|
+
return []
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _workspace_research_count(workspace_root: Path | str | None) -> int:
|
|
717
|
+
try:
|
|
718
|
+
return len(list_workspace_research_artifacts(Path(workspace_root or Path.cwd())))
|
|
719
|
+
except Exception:
|
|
720
|
+
return 0
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _latest_role_activity(role: str) -> list[dict[str, Any]]:
|
|
724
|
+
try:
|
|
725
|
+
from apps.mcp.models import McpToolCall
|
|
726
|
+
|
|
727
|
+
return [
|
|
728
|
+
{
|
|
729
|
+
"title": call.tool_name,
|
|
730
|
+
"status": call.status,
|
|
731
|
+
"status_class": _status_class(call.status),
|
|
732
|
+
"created_at": call.created_at,
|
|
733
|
+
}
|
|
734
|
+
for call in McpToolCall.objects.filter(principal_id=role).order_by("-created_at", "-id")[:5]
|
|
735
|
+
]
|
|
736
|
+
except Exception:
|
|
737
|
+
return []
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _model_count(module_name: str, class_name: str, **filters: Any) -> int:
|
|
741
|
+
try:
|
|
742
|
+
module = __import__(module_name, fromlist=[class_name])
|
|
743
|
+
model = getattr(module, class_name)
|
|
744
|
+
queryset = model.objects.filter(**filters) if filters else model.objects
|
|
745
|
+
return int(queryset.count())
|
|
746
|
+
except Exception:
|
|
747
|
+
return 0
|