aria-code 0.167.0__py3-none-win_amd64.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.
- aria_bots/__init__.py +7 -0
- aria_bots/channels/README.md +97 -0
- aria_bots/channels/__init__.py +1 -0
- aria_bots/channels/approvals.py +263 -0
- aria_bots/channels/conversation.py +280 -0
- aria_bots/channels/digest.py +186 -0
- aria_bots/channels/registry.py +95 -0
- aria_bots/engine.py +144 -0
- aria_bots/feishu_bot.py +1385 -0
- aria_bots/logistics/__init__.py +1 -0
- aria_bots/logistics/carriers.py +253 -0
- aria_bots/logistics/freight.py +154 -0
- aria_bots/logistics/inventory.py +242 -0
- aria_bots/logistics/mcp_server.py +156 -0
- aria_bots/logistics/records.py +119 -0
- aria_bots/logistics/tenancy.py +152 -0
- aria_bots/market.py +91 -0
- aria_bots/relay_client.py +326 -0
- aria_bots/telegram_bot.py +299 -0
- aria_code/__init__.py +111 -0
- aria_code/_bin/aria-native.exe +0 -0
- aria_code/_bin/aria-runtime.exe +0 -0
- aria_code/_version.py +20 -0
- aria_code/account_memory.py +296 -0
- aria_code/account_session.py +166 -0
- aria_code/adk_apps/__init__.py +1 -0
- aria_code/adk_apps/aria_code_review/__init__.py +1 -0
- aria_code/adk_apps/aria_code_review/agent.py +32 -0
- aria_code/adk_apps/aria_research/__init__.py +1 -0
- aria_code/adk_apps/aria_research/agent.py +31 -0
- aria_code/agents/__init__.py +49 -0
- aria_code/agents/base.py +329 -0
- aria_code/agents/code_review.py +200 -0
- aria_code/agents/deep/__init__.py +37 -0
- aria_code/agents/deep/calibration_loop.py +144 -0
- aria_code/agents/deep/critic.py +125 -0
- aria_code/agents/deep/deepen.py +193 -0
- aria_code/agents/deep/models.py +149 -0
- aria_code/agents/deep/pipeline.py +164 -0
- aria_code/agents/deep/quant_fusion.py +192 -0
- aria_code/agents/deep/themes.py +95 -0
- aria_code/agents/deep/tiers.py +106 -0
- aria_code/agents/engineering/__init__.py +14 -0
- aria_code/agents/engineering/coder.py +222 -0
- aria_code/agents/engineering/tester.py +145 -0
- aria_code/agents/financial/__init__.py +21 -0
- aria_code/agents/financial/cashflow_burn.py +119 -0
- aria_code/agents/financial/catalyst.py +279 -0
- aria_code/agents/financial/corporate_finance.py +175 -0
- aria_code/agents/financial/debate.py +224 -0
- aria_code/agents/financial/earnings.py +315 -0
- aria_code/agents/financial/fundamental.py +178 -0
- aria_code/agents/financial/macro.py +111 -0
- aria_code/agents/financial/news.py +231 -0
- aria_code/agents/financial/risk.py +146 -0
- aria_code/agents/financial/sector.py +279 -0
- aria_code/agents/financial/strategist.py +121 -0
- aria_code/agents/financial/stripe_revenue.py +127 -0
- aria_code/agents/financial/synthesis.py +274 -0
- aria_code/agents/financial/technical.py +277 -0
- aria_code/agents/orchestrator.py +44 -0
- aria_code/agents/portfolio_agent.py +392 -0
- aria_code/agents/realty/__init__.py +62 -0
- aria_code/agents/realty/asset_diagnosis.py +152 -0
- aria_code/agents/realty/business_match.py +166 -0
- aria_code/agents/realty/cashflow_verify.py +209 -0
- aria_code/agents/realty/contract_rules.py +210 -0
- aria_code/agents/realty/energy_anomaly.py +189 -0
- aria_code/agents/realty/exit_settlement.py +208 -0
- aria_code/agents/realty/fulfillment_risk.py +206 -0
- aria_code/agents/realty/ops_optimize.py +160 -0
- aria_code/agents/realty/revenue_share.py +215 -0
- aria_code/agents/registry.py +168 -0
- aria_code/agents/signal_scheme.py +111 -0
- aria_code/agents/sports/__init__.py +0 -0
- aria_code/agents/sports/football_agent.py +197 -0
- aria_code/agents/supervisor.py +113 -0
- aria_code/agents/team.py +500 -0
- aria_code/agents/video/__init__.py +16 -0
- aria_code/agents/video/director.py +376 -0
- aria_code/agents/video/qa_agent.py +143 -0
- aria_code/agents/video/script_doctor.py +112 -0
- aria_code/agents/warehouse/__init__.py +25 -0
- aria_code/agents/warehouse/contracts.py +26 -0
- aria_code/agents/warehouse/dashboard.py +155 -0
- aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
- aria_code/agents/warehouse/inbound_exceptions.py +25 -0
- aria_code/agents/warehouse/inventory_health.py +24 -0
- aria_code/agents/warehouse/local_run.py +71 -0
- aria_code/agents/warehouse/local_snapshot.py +132 -0
- aria_code/agents/warehouse/logistics_cost.py +84 -0
- aria_code/agents/warehouse/logistics_sync.py +21 -0
- aria_code/agents/warehouse/navigation.schema.json +110 -0
- aria_code/agents/warehouse/task_agent.py +172 -0
- aria_code/agents/warehouse/workflow.py +31 -0
- aria_code/aliyun_data_client.py +4 -0
- aria_code/apps/README.md +12 -0
- aria_code/apps/__init__.py +2 -0
- aria_code/apps/channels/README.md +9 -0
- aria_code/apps/channels/__init__.py +17 -0
- aria_code/apps/channels/approvals.py +11 -0
- aria_code/apps/channels/conversation.py +11 -0
- aria_code/apps/channels/digest.py +11 -0
- aria_code/apps/channels/intake.py +92 -0
- aria_code/apps/channels/registry.py +11 -0
- aria_code/apps/channels/tradingview.py +94 -0
- aria_code/apps/cli/README.md +13 -0
- aria_code/apps/cli/__init__.py +2 -0
- aria_code/apps/cli/_optional.py +120 -0
- aria_code/apps/cli/app_server.py +656 -0
- aria_code/apps/cli/auto_update.py +125 -0
- aria_code/apps/cli/bootstrap.py +206 -0
- aria_code/apps/cli/broker_render.py +206 -0
- aria_code/apps/cli/chat_turn.py +1327 -0
- aria_code/apps/cli/cloud_models.py +231 -0
- aria_code/apps/cli/codegen_paths.py +29 -0
- aria_code/apps/cli/commands/__init__.py +34 -0
- aria_code/apps/cli/commands/_business_render.py +78 -0
- aria_code/apps/cli/commands/_ui.py +71 -0
- aria_code/apps/cli/commands/analysis_cmds.py +378 -0
- aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
- aria_code/apps/cli/commands/auth_cmds.py +215 -0
- aria_code/apps/cli/commands/backtest_cmds.py +2518 -0
- aria_code/apps/cli/commands/broker_cmds.py +1236 -0
- aria_code/apps/cli/commands/business_workflow_cmds.py +379 -0
- aria_code/apps/cli/commands/canvas_cmds.py +77 -0
- aria_code/apps/cli/commands/catalog.py +189 -0
- aria_code/apps/cli/commands/core_cmds.py +2748 -0
- aria_code/apps/cli/commands/data_cmds.py +521 -0
- aria_code/apps/cli/commands/diagnostic_cmds.py +285 -0
- aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
- aria_code/apps/cli/commands/engineering_cmds.py +399 -0
- aria_code/apps/cli/commands/file_cmds.py +312 -0
- aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
- aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
- aria_code/apps/cli/commands/market.py +544 -0
- aria_code/apps/cli/commands/market_cmds.py +1419 -0
- aria_code/apps/cli/commands/market_context.py +432 -0
- aria_code/apps/cli/commands/market_render.py +7 -0
- aria_code/apps/cli/commands/model_cmds.py +2374 -0
- aria_code/apps/cli/commands/ops_cmds.py +721 -0
- aria_code/apps/cli/commands/orchestrator_cmds.py +181 -0
- aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
- aria_code/apps/cli/commands/portfolio_cmds.py +1316 -0
- aria_code/apps/cli/commands/report.py +476 -0
- aria_code/apps/cli/commands/scaffold_templates.py +617 -0
- aria_code/apps/cli/commands/session_cmds.py +236 -0
- aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
- aria_code/apps/cli/commands/team.py +723 -0
- aria_code/apps/cli/commands/team_render.py +8 -0
- aria_code/apps/cli/commands/ui_cmds.py +401 -0
- aria_code/apps/cli/commands/warehouse_cmds.py +297 -0
- aria_code/apps/cli/commands/workflow_cmds.py +629 -0
- aria_code/apps/cli/commands/workspace_cmds.py +1543 -0
- aria_code/apps/cli/config_paths.py +73 -0
- aria_code/apps/cli/config_store.py +158 -0
- aria_code/apps/cli/constants.py +88 -0
- aria_code/apps/cli/context.py +34 -0
- aria_code/apps/cli/conversation_policy.py +53 -0
- aria_code/apps/cli/deterministic.py +200 -0
- aria_code/apps/cli/direct.py +48 -0
- aria_code/apps/cli/earnings_view.py +172 -0
- aria_code/apps/cli/endpoint_migration.py +64 -0
- aria_code/apps/cli/exec_events.py +255 -0
- aria_code/apps/cli/file_references.py +155 -0
- aria_code/apps/cli/finance_service_catalog.py +111 -0
- aria_code/apps/cli/football_reports.py +231 -0
- aria_code/apps/cli/github_app_auth.py +135 -0
- aria_code/apps/cli/google_login.py +239 -0
- aria_code/apps/cli/handlers/__init__.py +11 -0
- aria_code/apps/cli/handlers/broker_handlers.py +122 -0
- aria_code/apps/cli/handlers/chart_handlers.py +1314 -0
- aria_code/apps/cli/handlers/market_handlers.py +2570 -0
- aria_code/apps/cli/handlers/realty_handlers.py +114 -0
- aria_code/apps/cli/handlers/sports_handlers.py +22 -0
- aria_code/apps/cli/handlers/strategy_advice.py +82 -0
- aria_code/apps/cli/headless.py +352 -0
- aria_code/apps/cli/helpers.py +326 -0
- aria_code/apps/cli/hooks.py +180 -0
- aria_code/apps/cli/i18n.py +310 -0
- aria_code/apps/cli/intent.py +136 -0
- aria_code/apps/cli/intent_router.py +305 -0
- aria_code/apps/cli/intent_shadow.py +72 -0
- aria_code/apps/cli/intent_signals.py +347 -0
- aria_code/apps/cli/lifecycle_hooks.py +48 -0
- aria_code/apps/cli/local_tool_schemas.py +550 -0
- aria_code/apps/cli/main.py +39 -0
- aria_code/apps/cli/market_hours.py +100 -0
- aria_code/apps/cli/market_metadata.py +135 -0
- aria_code/apps/cli/market_universe.py +487 -0
- aria_code/apps/cli/message_processing.py +310 -0
- aria_code/apps/cli/mixin_binding.py +32 -0
- aria_code/apps/cli/model_catalog.py +652 -0
- aria_code/apps/cli/model_probe.py +169 -0
- aria_code/apps/cli/model_retirement.py +65 -0
- aria_code/apps/cli/native_branding.py +12 -0
- aria_code/apps/cli/native_bridge.py +118 -0
- aria_code/apps/cli/native_entry.py +58 -0
- aria_code/apps/cli/pdf_report.py +977 -0
- aria_code/apps/cli/plan_mode.py +139 -0
- aria_code/apps/cli/plotly_html.py +15 -0
- aria_code/apps/cli/prediction_feedback.py +202 -0
- aria_code/apps/cli/preflight.py +502 -0
- aria_code/apps/cli/project_aria.py +60 -0
- aria_code/apps/cli/prompt_assembly.py +78 -0
- aria_code/apps/cli/prompts/__init__.py +0 -0
- aria_code/apps/cli/prompts/coding.py +71 -0
- aria_code/apps/cli/prompts/select.py +193 -0
- aria_code/apps/cli/prompts/system_prompts.py +562 -0
- aria_code/apps/cli/prompts/ui.py +436 -0
- aria_code/apps/cli/provider_endpoints.py +237 -0
- aria_code/apps/cli/provider_keys.py +73 -0
- aria_code/apps/cli/providers/__init__.py +1 -0
- aria_code/apps/cli/providers/base.py +775 -0
- aria_code/apps/cli/providers/chat_routing.py +139 -0
- aria_code/apps/cli/providers/collaboration.py +98 -0
- aria_code/apps/cli/providers/llm/__init__.py +1 -0
- aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
- aria_code/apps/cli/providers/llm/ollama_stream.py +1574 -0
- aria_code/apps/cli/providers/llm/response_cache.py +60 -0
- aria_code/apps/cli/providers/llm/sse_stream.py +298 -0
- aria_code/apps/cli/providers/runtime_bridge.py +586 -0
- aria_code/apps/cli/providers/vertexai_stream.py +592 -0
- aria_code/apps/cli/release_download.py +159 -0
- aria_code/apps/cli/review_runner.py +111 -0
- aria_code/apps/cli/runtime_consumer.py +769 -0
- aria_code/apps/cli/sandbox.py +32 -0
- aria_code/apps/cli/session_export.py +120 -0
- aria_code/apps/cli/session_jsonl.py +208 -0
- aria_code/apps/cli/session_refresh.py +123 -0
- aria_code/apps/cli/session_store.py +125 -0
- aria_code/apps/cli/skills_catalog.py +273 -0
- aria_code/apps/cli/task_isolation.py +211 -0
- aria_code/apps/cli/terminal_chrome.py +412 -0
- aria_code/apps/cli/todo_tracker.py +190 -0
- aria_code/apps/cli/tool_executor.py +932 -0
- aria_code/apps/cli/tool_registry.py +34 -0
- aria_code/apps/cli/tool_scope.py +113 -0
- aria_code/apps/cli/tools/__init__.py +40 -0
- aria_code/apps/cli/tools/context.py +46 -0
- aria_code/apps/cli/tools/file_tools.py +181 -0
- aria_code/apps/cli/tools/market_tools.py +626 -0
- aria_code/apps/cli/tools/notebook_tools.py +111 -0
- aria_code/apps/cli/tools/system_tools.py +656 -0
- aria_code/apps/cli/tools/write_tools.py +961 -0
- aria_code/apps/cli/tradingview_bridge.py +529 -0
- aria_code/apps/cli/transactions.py +269 -0
- aria_code/apps/cli/turn_planning.py +137 -0
- aria_code/apps/cli/update_check.py +257 -0
- aria_code/apps/cli/updater.py +373 -0
- aria_code/apps/cli/utils/__init__.py +0 -0
- aria_code/apps/cli/utils/market_detect.py +1663 -0
- aria_code/apps/cli/web_login.py +136 -0
- aria_code/apps/cli/workflow_runner.py +127 -0
- aria_code/apps/cli/workspace_route.py +121 -0
- aria_code/apps/daemon/README.md +14 -0
- aria_code/apps/vscode/README.md +115 -0
- aria_code/apps/vscode/package.json +70 -0
- aria_code/aria_cli.py +5244 -0
- aria_code/aria_daemon.py +1372 -0
- aria_code/aria_feishu_bot.py +16 -0
- aria_code/aria_mcp_server.py +61 -0
- aria_code/aria_relay_client.py +4 -0
- aria_code/aria_telegram_bot.py +11 -0
- aria_code/ariarc.py +385 -0
- aria_code/artifacts.py +581 -0
- aria_code/backtest_engine.py +4 -0
- aria_code/backtest_report.py +4 -0
- aria_code/brokers/__init__.py +78 -0
- aria_code/brokers/automation.py +193 -0
- aria_code/brokers/base.py +231 -0
- aria_code/brokers/capabilities.py +264 -0
- aria_code/brokers/cn/__init__.py +10 -0
- aria_code/brokers/cn/easytrader_broker.py +193 -0
- aria_code/brokers/cn/futu_broker.py +194 -0
- aria_code/brokers/cn/longbridge_broker.py +190 -0
- aria_code/brokers/cn/tiger_broker.py +196 -0
- aria_code/brokers/cn/xtquant_broker.py +225 -0
- aria_code/brokers/config.py +414 -0
- aria_code/brokers/intl/__init__.py +5 -0
- aria_code/brokers/intl/alpaca_broker.py +183 -0
- aria_code/brokers/intl/ibkr_broker.py +215 -0
- aria_code/brokers/intl/trading212_broker.py +183 -0
- aria_code/brokers/intl/webull_broker.py +156 -0
- aria_code/brokers/paper_broker.py +266 -0
- aria_code/brokers/planning.py +296 -0
- aria_code/brokers/registry.py +213 -0
- aria_code/brokers/trading.py +267 -0
- aria_code/canva_client.py +4 -0
- aria_code/change_store.py +127 -0
- aria_code/clients/__init__.py +0 -0
- aria_code/clients/aliyun_data_client.py +660 -0
- aria_code/clients/aria_relay_client.py +16 -0
- aria_code/clients/canva_client.py +284 -0
- aria_code/clients/figma_client.py +100 -0
- aria_code/clients/football_data_client.py +1671 -0
- aria_code/clients/kling_video_client.py +182 -0
- aria_code/clients/market_data_client.py +1985 -0
- aria_code/clients/openai_image_client.py +242 -0
- aria_code/clients/runway_video_client.py +157 -0
- aria_code/clients/warehouse_erp_client.py +105 -0
- aria_code/cloud_memory.py +190 -0
- aria_code/command_safety.py +19 -0
- aria_code/computer_use_server.py +276 -0
- aria_code/computer_use_tools.py +4 -0
- aria_code/dashboard_generator.py +579 -0
- aria_code/data_analysis_tools.py +4 -0
- aria_code/data_cleaner.py +483 -0
- aria_code/data_service.py +513 -0
- aria_code/datasources/__init__.py +23 -0
- aria_code/datasources/base.py +166 -0
- aria_code/datasources/router.py +253 -0
- aria_code/datasources/sec_statements.py +217 -0
- aria_code/datasources/sources/__init__.py +15 -0
- aria_code/datasources/sources/akshare_source.py +269 -0
- aria_code/datasources/sources/alpha_vantage_source.py +203 -0
- aria_code/datasources/sources/edgar_source.py +219 -0
- aria_code/datasources/sources/finnhub_source.py +198 -0
- aria_code/datasources/sources/fred_source.py +223 -0
- aria_code/datasources/sources/tushare_source.py +143 -0
- aria_code/datasources/sources/web_scraper_source.py +278 -0
- aria_code/datasources/sources/world_bank_source.py +206 -0
- aria_code/datasources/sources/yfinance_source.py +152 -0
- aria_code/doctor.py +860 -0
- aria_code/domain/__init__.py +0 -0
- aria_code/domain/_finance_core/__init__.py +5 -0
- aria_code/domain/_finance_core/backtest_engine.py +325 -0
- aria_code/domain/_finance_core/performance_stats.py +101 -0
- aria_code/domain/backtest_engine.py +75 -0
- aria_code/domain/backtest_report.py +488 -0
- aria_code/domain/finance_formulas.py +389 -0
- aria_code/domain/performance_stats.py +6 -0
- aria_code/domain/portfolio_ledger.py +307 -0
- aria_code/domain/strategy_vault.py +421 -0
- aria_code/evals/__init__.py +37 -0
- aria_code/evals/behavior.py +113 -0
- aria_code/evals/gap.py +93 -0
- aria_code/evals/harness.py +949 -0
- aria_code/evals/review_probe.py +132 -0
- aria_code/evals/runner.py +425 -0
- aria_code/evals/scorecard.py +322 -0
- aria_code/evals/summary.py +125 -0
- aria_code/external_agent_runner.py +115 -0
- aria_code/figma_client.py +4 -0
- aria_code/file_analysis_tools.py +4 -0
- aria_code/finance_formulas.py +4 -0
- aria_code/football_data_client.py +4 -0
- aria_code/image_gen_tools.py +4 -0
- aria_code/intent_classifier.py +459 -0
- aria_code/kling_video_client.py +4 -0
- aria_code/licensing.py +151 -0
- aria_code/local_finance_tools.py +4 -0
- aria_code/local_image_provider.py +4 -0
- aria_code/local_llm_provider.py +4 -0
- aria_code/macro_tools.py +4 -0
- aria_code/markdown_pdf.py +289 -0
- aria_code/market_data_client.py +4 -0
- aria_code/mcp_client.py +744 -0
- aria_code/memory_manager.py +273 -0
- aria_code/model_capability.py +512 -0
- aria_code/notification_tools.py +4 -0
- aria_code/openai_image_client.py +4 -0
- aria_code/packages/__init__.py +26 -0
- aria_code/packages/adk_bridge/__init__.py +11 -0
- aria_code/packages/adk_bridge/code_review_tools.py +49 -0
- aria_code/packages/adk_bridge/market_tools.py +125 -0
- aria_code/packages/aria_agents/__init__.py +5 -0
- aria_code/packages/aria_agents/manifest.py +78 -0
- aria_code/packages/aria_core/__init__.py +34 -0
- aria_code/packages/aria_core/architecture.py +269 -0
- aria_code/packages/aria_core/export.py +147 -0
- aria_code/packages/aria_core/manifest.py +65 -0
- aria_code/packages/aria_core/paths.py +57 -0
- aria_code/packages/aria_core/secure_file.py +68 -0
- aria_code/packages/aria_infra/__init__.py +15 -0
- aria_code/packages/aria_infra/arthera.py +58 -0
- aria_code/packages/aria_infra/doctor.py +246 -0
- aria_code/packages/aria_infra/product.py +40 -0
- aria_code/packages/aria_mcp/__init__.py +25 -0
- aria_code/packages/aria_mcp/bridge.py +232 -0
- aria_code/packages/aria_mcp/circuit.py +82 -0
- aria_code/packages/aria_mcp/config.py +162 -0
- aria_code/packages/aria_mcp/installer.py +95 -0
- aria_code/packages/aria_mcp/server.py +1705 -0
- aria_code/packages/aria_mcp/tools.py +81 -0
- aria_code/packages/aria_sdk/__init__.py +19 -0
- aria_code/packages/aria_sdk/client.py +393 -0
- aria_code/packages/aria_sdk/providers.py +70 -0
- aria_code/packages/aria_sdk/streaming.py +75 -0
- aria_code/packages/aria_sdk/types.py +86 -0
- aria_code/packages/aria_services/__init__.py +92 -0
- aria_code/packages/aria_services/cache.py +60 -0
- aria_code/packages/aria_services/context.py +356 -0
- aria_code/packages/aria_services/context_checkpoints.py +157 -0
- aria_code/packages/aria_services/data.py +11 -0
- aria_code/packages/aria_services/provider_health.py +304 -0
- aria_code/packages/aria_services/references.py +247 -0
- aria_code/packages/aria_services/registry.py +249 -0
- aria_code/packages/aria_services/research_protocol.py +137 -0
- aria_code/packages/aria_services/research_quality.py +186 -0
- aria_code/packages/aria_services/settings.py +160 -0
- aria_code/packages/aria_services/usage.py +151 -0
- aria_code/packages/aria_skills/__init__.py +46 -0
- aria_code/packages/aria_skills/catalog.py +149 -0
- aria_code/packages/aria_skills/loader.py +455 -0
- aria_code/packages/aria_skills/registry.py +84 -0
- aria_code/packages/aria_tools/__init__.py +5 -0
- aria_code/packages/aria_tools/financial/__init__.py +5 -0
- aria_code/packages/aria_tools/financial/_paths.py +92 -0
- aria_code/packages/aria_tools/registry.py +129 -0
- aria_code/packages/quant_engine/__init__.py +33 -0
- aria_code/packages/quant_engine/_namespace.py +24 -0
- aria_code/packages/quant_engine/backtest/__init__.py +10 -0
- aria_code/packages/quant_engine/backtest/core.py +53 -0
- aria_code/packages/quant_engine/backtest/engine.py +102 -0
- aria_code/packages/quant_engine/portfolio/__init__.py +9 -0
- aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
- aria_code/packages/quant_engine/sports/__init__.py +76 -0
- aria_code/packages/quant_engine/sports/calibrator.py +354 -0
- aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
- aria_code/packages/quant_engine/sports/elo.py +300 -0
- aria_code/packages/quant_engine/sports/form.py +188 -0
- aria_code/packages/quant_engine/sports/h2h.py +195 -0
- aria_code/packages/quant_engine/sports/predictor.py +312 -0
- aria_code/packages/quant_engine/sports/tracker.py +665 -0
- aria_code/packages/quant_engine/stochastic/__init__.py +31 -0
- aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
- aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
- aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- aria_code/packs/__init__.py +85 -0
- aria_code/packs/base.py +168 -0
- aria_code/packs/finance/__init__.py +250 -0
- aria_code/packs/healthcare/__init__.py +40 -0
- aria_code/packs/logistics/__init__.py +171 -0
- aria_code/packs/payments/__init__.py +139 -0
- aria_code/packs/realty/__init__.py +190 -0
- aria_code/packs/registry.py +202 -0
- aria_code/packs/rules.py +90 -0
- aria_code/plan_utils.py +194 -0
- aria_code/plugin_loader.py +328 -0
- aria_code/portfolio_ledger.py +4 -0
- aria_code/preview_server.py +449 -0
- aria_code/privacy/__init__.py +5 -0
- aria_code/privacy/feedback.py +123 -0
- aria_code/project_review.py +278 -0
- aria_code/project_review_api.py +235 -0
- aria_code/project_review_server.py +18 -0
- aria_code/project_tools.py +4 -0
- aria_code/providers/__init__.py +30 -0
- aria_code/providers/google_video_provider.py +256 -0
- aria_code/providers/llm/__init__.py +19 -0
- aria_code/providers/llm/anthropic.py +184 -0
- aria_code/providers/llm/arthera_gateway.py +90 -0
- aria_code/providers/llm/autoconfig.py +213 -0
- aria_code/providers/llm/base.py +139 -0
- aria_code/providers/llm/custom.py +158 -0
- aria_code/providers/llm/ollama.py +137 -0
- aria_code/providers/llm/openai_compat.py +350 -0
- aria_code/providers/llm/registry.py +457 -0
- aria_code/providers/local_image_provider.py +202 -0
- aria_code/providers/local_llm_provider.py +549 -0
- aria_code/providers/ollama_models.py +79 -0
- aria_code/providers/openai_params.py +32 -0
- aria_code/providers/tool_messages.py +100 -0
- aria_code/realty_data_tools.py +4 -0
- aria_code/report_exporters.py +150 -0
- aria_code/report_generator.py +1764 -0
- aria_code/review_service.py +497 -0
- aria_code/runtime/__init__.py +176 -0
- aria_code/runtime/acceptance.py +632 -0
- aria_code/runtime/agent_loop.py +1851 -0
- aria_code/runtime/agent_profiles.py +75 -0
- aria_code/runtime/approval.py +71 -0
- aria_code/runtime/baseline.py +219 -0
- aria_code/runtime/budget.py +232 -0
- aria_code/runtime/checkpoints.py +481 -0
- aria_code/runtime/consult.py +65 -0
- aria_code/runtime/contract.py +282 -0
- aria_code/runtime/delivery.py +432 -0
- aria_code/runtime/events.py +113 -0
- aria_code/runtime/gateway.py +167 -0
- aria_code/runtime/lsp.py +484 -0
- aria_code/runtime/native_index.py +153 -0
- aria_code/runtime/orchestrator.py +55 -0
- aria_code/runtime/processes.py +343 -0
- aria_code/runtime/project_graph.py +614 -0
- aria_code/runtime/repo_map.py +712 -0
- aria_code/runtime/research_bundle.py +105 -0
- aria_code/runtime/review.py +269 -0
- aria_code/runtime/run_state.py +127 -0
- aria_code/runtime/run_store.py +522 -0
- aria_code/runtime/scoped_runs.py +113 -0
- aria_code/runtime/self_healing.py +422 -0
- aria_code/runtime/semantic_diff.py +171 -0
- aria_code/runtime/subagent.py +721 -0
- aria_code/runtime/symbol_edit.py +117 -0
- aria_code/runtime/task_brief.py +46 -0
- aria_code/runtime/task_context.py +145 -0
- aria_code/runtime/task_ledger.py +108 -0
- aria_code/runtime/task_orchestrator.py +90 -0
- aria_code/runtime/task_outcome.py +33 -0
- aria_code/runtime/task_verification.py +266 -0
- aria_code/runtime/task_worktree.py +448 -0
- aria_code/runtime/tool_executor.py +329 -0
- aria_code/runtime/tool_policy.py +107 -0
- aria_code/runtime/transactions.py +216 -0
- aria_code/runtime/windows_job.py +115 -0
- aria_code/runtime/workflows.py +222 -0
- aria_code/runtime/worktrees.py +215 -0
- aria_code/runway_video_client.py +4 -0
- aria_code/safety/__init__.py +27 -0
- aria_code/safety/client_isolation.py +178 -0
- aria_code/safety/permissions.py +339 -0
- aria_code/safety/risk.py +699 -0
- aria_code/safety/sandbox.py +148 -0
- aria_code/safety/service.py +102 -0
- aria_code/service_endpoints.py +14 -0
- aria_code/setup_wizard.py +684 -0
- aria_code/spreadsheet_tools.py +4 -0
- aria_code/strategy_vault.py +4 -0
- aria_code/tools/__init__.py +0 -0
- aria_code/tools/artifact_tools.py +252 -0
- aria_code/tools/broker_tools.py +92 -0
- aria_code/tools/build_quant_engine.py +97 -0
- aria_code/tools/code_audit_tools.py +192 -0
- aria_code/tools/computer_use_tools.py +509 -0
- aria_code/tools/data_analysis_tools.py +831 -0
- aria_code/tools/enterprise_finance_tools.py +202 -0
- aria_code/tools/extended_tools.py +240 -0
- aria_code/tools/file_analysis_tools.py +880 -0
- aria_code/tools/image_gen_tools.py +114 -0
- aria_code/tools/local_finance_tools.py +3488 -0
- aria_code/tools/logistics_carriers.py +11 -0
- aria_code/tools/logistics_inventory.py +11 -0
- aria_code/tools/logistics_io.py +11 -0
- aria_code/tools/logistics_tenancy.py +11 -0
- aria_code/tools/logistics_tools.py +11 -0
- aria_code/tools/macro_tools.py +412 -0
- aria_code/tools/notification_tools.py +249 -0
- aria_code/tools/project_tools.py +525 -0
- aria_code/tools/realty_data_tools.py +662 -0
- aria_code/tools/spreadsheet_tools.py +399 -0
- aria_code/tools/stripe_tools.py +115 -0
- aria_code/tools/video_tools.py +500 -0
- aria_code/training/__init__.py +1 -0
- aria_code/training/compare.py +117 -0
- aria_code/training/prelude_commands.py +111 -0
- aria_code/training/prelude_eval.py +390 -0
- aria_code/training/prelude_real.py +183 -0
- aria_code/training/prelude_train.py +248 -0
- aria_code/training/sft_dataset.py +230 -0
- aria_code/training/trajectory_dataset.py +263 -0
- aria_code/ui/__init__.py +105 -0
- aria_code/ui/banner.py +404 -0
- aria_code/ui/completer.py +571 -0
- aria_code/ui/console.py +285 -0
- aria_code/ui/input_box.py +505 -0
- aria_code/ui/mentions.py +206 -0
- aria_code/ui/picker.py +248 -0
- aria_code/ui/render/__init__.py +11 -0
- aria_code/ui/render/actions.py +297 -0
- aria_code/ui/render/finance.py +1732 -0
- aria_code/ui/render/market.py +253 -0
- aria_code/ui/render/output.py +954 -0
- aria_code/ui/render/responsive.py +75 -0
- aria_code/ui/render/team.py +367 -0
- aria_code/ui/robot.py +239 -0
- aria_code/ui/startup_dashboard.py +110 -0
- aria_code/video_analysis.py +147 -0
- aria_code/video_editor.py +518 -0
- aria_code/video_workflow/__init__.py +8 -0
- aria_code/video_workflow/__main__.py +61 -0
- aria_code/video_workflow/engines/__init__.py +18 -0
- aria_code/video_workflow/engines/base.py +69 -0
- aria_code/video_workflow/engines/custom_model.py +204 -0
- aria_code/video_workflow/engines/google_veo.py +94 -0
- aria_code/video_workflow/engines/local_engine.py +71 -0
- aria_code/video_workflow/engines/registry.py +98 -0
- aria_code/video_workflow/google_models.py +278 -0
- aria_code/video_workflow/music/__init__.py +5 -0
- aria_code/video_workflow/music/foley_service.py +132 -0
- aria_code/video_workflow/music/service.py +212 -0
- aria_code/video_workflow/pipeline.py +620 -0
- aria_code/video_workflow/plan.py +202 -0
- aria_code/video_workflow/session.py +297 -0
- aria_code/workspace/__init__.py +6 -0
- aria_code/workspace/files.py +219 -0
- aria_code/workspace/verify.py +167 -0
- aria_code/workspace_changes.py +199 -0
- aria_code/workspace_service.py +196 -0
- aria_code/workspace_tools.py +150 -0
- aria_code-0.167.0.dist-info/METADATA +330 -0
- aria_code-0.167.0.dist-info/RECORD +601 -0
- aria_code-0.167.0.dist-info/WHEEL +5 -0
- aria_code-0.167.0.dist-info/entry_points.txt +5 -0
- aria_code-0.167.0.dist-info/licenses/LICENSE +201 -0
- aria_code-0.167.0.dist-info/licenses/NOTICE +49 -0
- aria_code-0.167.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Logistics tools: freight, carrier scoring and inventory policy, scoped per shipper."""
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Carrier scorecards, freight anomalies and savings for a 3PL, by lane.
|
|
2
|
+
|
|
3
|
+
A raw on-time percentage ranks a carrier with 1 of 1 deliveries on time above
|
|
4
|
+
one with 98 of 100, which is backwards. Carriers here are ranked by the lower
|
|
5
|
+
bound of the Wilson score interval for their on-time rate, which is what the
|
|
6
|
+
data actually supports: a carrier earns a high rank by being on time *often*,
|
|
7
|
+
not by being on time once.
|
|
8
|
+
|
|
9
|
+
Wilson lower bound = (p̂ + z²/2n − z·√(p̂(1−p̂)/n + z²/4n²)) / (1 + z²/n)
|
|
10
|
+
|
|
11
|
+
Freight anomalies use the modified z-score of cost per kg within a lane —
|
|
12
|
+
median and median absolute deviation, so one outlier cannot hide itself by
|
|
13
|
+
inflating the mean it is compared against (Iglewicz & Hoaglin, |Mᵢ| > 3.5):
|
|
14
|
+
|
|
15
|
+
Mᵢ = 0.6745 · (xᵢ − median) / MAD
|
|
16
|
+
Mᵢ = (xᵢ − median) / (1.253314 · MeanAD) when MAD = 0
|
|
17
|
+
|
|
18
|
+
The second form matters: on a contracted lane most shipments share one rate,
|
|
19
|
+
which makes MAD zero, and those are the lanes where an overcharge is plainest.
|
|
20
|
+
|
|
21
|
+
A savings opportunity is reported only when, on the same lane, an alternative
|
|
22
|
+
carrier is both cheaper per kg *and* at least as reliable by the same lower
|
|
23
|
+
bound, with enough shipments on both sides to say so. The estimate is volume
|
|
24
|
+
× rate difference and nothing more; contract terms, surcharges, minimums and
|
|
25
|
+
capacity are not modelled and the result says so.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import math
|
|
31
|
+
import statistics
|
|
32
|
+
from collections import defaultdict
|
|
33
|
+
from statistics import NormalDist
|
|
34
|
+
from typing import Any, Dict, List
|
|
35
|
+
|
|
36
|
+
from .records import flag, load_records, number, text
|
|
37
|
+
from .tenancy import TenancyError, owner_params_schema, scope_records
|
|
38
|
+
|
|
39
|
+
DEFAULTS = {"confidence": 0.95, "min_shipments": 20}
|
|
40
|
+
ANOMALY_THRESHOLD = 3.5
|
|
41
|
+
OVERBILLED_WEIGHT_RATIO = 1.2
|
|
42
|
+
MIN_LANE_ROWS_FOR_OUTLIERS = 5
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def wilson_lower_bound(successes: int, trials: int, confidence: float = 0.95) -> float | None:
|
|
46
|
+
if trials <= 0:
|
|
47
|
+
return None
|
|
48
|
+
z = NormalDist().inv_cdf(1 - (1 - confidence) / 2)
|
|
49
|
+
p = successes / trials
|
|
50
|
+
denominator = 1 + z * z / trials
|
|
51
|
+
centre = p + z * z / (2 * trials)
|
|
52
|
+
margin = z * math.sqrt(p * (1 - p) / trials + z * z / (4 * trials * trials))
|
|
53
|
+
return (centre - margin) / denominator
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _count(n: int, one: str, many: str) -> str:
|
|
57
|
+
return f"{n} {one if n == 1 else many}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _lane(record: Dict[str, Any], row: int) -> str:
|
|
61
|
+
origin = text(record, "origin", row)
|
|
62
|
+
destination = text(record, "destination", row)
|
|
63
|
+
return f"{origin}→{destination}" if origin and destination else "(all lanes)"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def tool_score_carriers(params: Dict[str, Any]) -> Dict[str, Any]:
|
|
67
|
+
try:
|
|
68
|
+
records, source = load_records(params, "waybills")
|
|
69
|
+
records, scope = scope_records(records, params.get("owner_id"),
|
|
70
|
+
all_owners=bool(params.get("all_owners")))
|
|
71
|
+
confidence = float(params.get("confidence", DEFAULTS["confidence"]))
|
|
72
|
+
if not 0.5 <= confidence < 1:
|
|
73
|
+
raise ValueError("confidence must be at least 0.5 and below 1")
|
|
74
|
+
min_shipments = int(params.get("min_shipments", DEFAULTS["min_shipments"]))
|
|
75
|
+
if min_shipments < 1:
|
|
76
|
+
raise ValueError("min_shipments must be at least 1")
|
|
77
|
+
|
|
78
|
+
cells: Dict[tuple[str, str], Dict[str, Any]] = defaultdict(lambda: {
|
|
79
|
+
"shipments": 0, "total_cost": 0.0, "total_kg": 0.0, "cost_per_kg": [],
|
|
80
|
+
"on_time_known": 0, "on_time": 0, "exception_known": 0, "exceptions": 0,
|
|
81
|
+
})
|
|
82
|
+
lane_rates: Dict[str, List[tuple[float, Dict[str, Any], str]]] = defaultdict(list)
|
|
83
|
+
anomalies: List[Dict[str, Any]] = []
|
|
84
|
+
|
|
85
|
+
for row, record in enumerate(records, 1):
|
|
86
|
+
carrier = text(record, "carrier", row, required=True)
|
|
87
|
+
cost = number(record, "total_cost", row, required=True)
|
|
88
|
+
billed = number(record, "billed_weight_kg", row)
|
|
89
|
+
actual = number(record, "actual_weight_kg", row)
|
|
90
|
+
on_time = flag(record, "is_on_time", row)
|
|
91
|
+
exception = flag(record, "has_exception", row)
|
|
92
|
+
lane = _lane(record, row)
|
|
93
|
+
waybill = text(record, "waybill_no", row) or f"row {row}"
|
|
94
|
+
|
|
95
|
+
cell = cells[(lane, carrier)]
|
|
96
|
+
cell["shipments"] += 1
|
|
97
|
+
cell["total_cost"] += cost
|
|
98
|
+
weight = billed if billed else actual
|
|
99
|
+
if weight:
|
|
100
|
+
cell["total_kg"] += weight
|
|
101
|
+
rate = cost / weight
|
|
102
|
+
cell["cost_per_kg"].append(rate)
|
|
103
|
+
lane_rates[lane].append((rate, {"waybill_no": waybill, "carrier": carrier}, carrier))
|
|
104
|
+
if on_time is not None:
|
|
105
|
+
cell["on_time_known"] += 1
|
|
106
|
+
cell["on_time"] += int(on_time)
|
|
107
|
+
if exception is not None:
|
|
108
|
+
cell["exception_known"] += 1
|
|
109
|
+
cell["exceptions"] += int(exception)
|
|
110
|
+
if actual and billed and billed > actual * OVERBILLED_WEIGHT_RATIO:
|
|
111
|
+
anomalies.append({
|
|
112
|
+
"waybill_no": waybill, "carrier": carrier, "lane": lane,
|
|
113
|
+
"kind": "billed_weight",
|
|
114
|
+
"detail": f"billed {billed:g} kg vs actual {actual:g} kg "
|
|
115
|
+
f"(> {OVERBILLED_WEIGHT_RATIO - 1:.0%} over); check dimensional weight before disputing",
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
# Cost-per-kg outliers within each lane, expensive side only.
|
|
119
|
+
for lane, rows in lane_rates.items():
|
|
120
|
+
if len(rows) < MIN_LANE_ROWS_FOR_OUTLIERS:
|
|
121
|
+
continue
|
|
122
|
+
rates = [r for r, _, _ in rows]
|
|
123
|
+
median = statistics.median(rates)
|
|
124
|
+
deviations = [abs(r - median) for r in rates]
|
|
125
|
+
mad = statistics.median(deviations)
|
|
126
|
+
# When more than half the shipments share one rate — the normal
|
|
127
|
+
# case on a contracted lane — MAD is 0 and the modified z-score
|
|
128
|
+
# is undefined. Skipping the lane there would let a 6x overcharge
|
|
129
|
+
# through on exactly the lanes where one is easiest to see. Fall
|
|
130
|
+
# back to the mean absolute deviation, scaled so the threshold
|
|
131
|
+
# means the same thing (1.253314 · MeanAD estimates σ).
|
|
132
|
+
if mad > 0:
|
|
133
|
+
scale = mad / 0.6745
|
|
134
|
+
else:
|
|
135
|
+
mean_ad = statistics.fmean(deviations)
|
|
136
|
+
if mean_ad == 0:
|
|
137
|
+
continue # every shipment on the lane has the same rate
|
|
138
|
+
scale = 1.253314 * mean_ad
|
|
139
|
+
for rate, meta, carrier in rows:
|
|
140
|
+
score = (rate - median) / scale
|
|
141
|
+
if score > ANOMALY_THRESHOLD:
|
|
142
|
+
anomalies.append({
|
|
143
|
+
**meta, "lane": lane, "kind": "cost_per_kg",
|
|
144
|
+
"detail": f"{rate:.2f}/kg vs lane median {median:.2f}/kg (modified z {score:.1f})",
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
scorecard: List[Dict[str, Any]] = []
|
|
148
|
+
for (lane, carrier), cell in cells.items():
|
|
149
|
+
lower = wilson_lower_bound(cell["on_time"], cell["on_time_known"], confidence)
|
|
150
|
+
scorecard.append({
|
|
151
|
+
"lane": lane,
|
|
152
|
+
"carrier": carrier,
|
|
153
|
+
"shipments": cell["shipments"],
|
|
154
|
+
"on_time_rate": round(cell["on_time"] / cell["on_time_known"], 4) if cell["on_time_known"] else None,
|
|
155
|
+
"on_time_lower_bound": round(lower, 4) if lower is not None else None,
|
|
156
|
+
"on_time_sample": cell["on_time_known"],
|
|
157
|
+
"median_cost_per_kg": round(statistics.median(cell["cost_per_kg"]), 4) if cell["cost_per_kg"] else None,
|
|
158
|
+
"total_cost": round(cell["total_cost"], 2),
|
|
159
|
+
"total_kg": round(cell["total_kg"], 2),
|
|
160
|
+
"exception_rate": round(cell["exceptions"] / cell["exception_known"], 4) if cell["exception_known"] else None,
|
|
161
|
+
"rankable": cell["shipments"] >= min_shipments and lower is not None,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
# Rank within each lane: reliability first, then price.
|
|
165
|
+
for lane in {c["lane"] for c in scorecard}:
|
|
166
|
+
ranked = sorted(
|
|
167
|
+
(c for c in scorecard if c["lane"] == lane and c["rankable"]),
|
|
168
|
+
key=lambda c: (-c["on_time_lower_bound"],
|
|
169
|
+
c["median_cost_per_kg"] if c["median_cost_per_kg"] is not None else math.inf,
|
|
170
|
+
c["carrier"]),
|
|
171
|
+
)
|
|
172
|
+
for position, entry in enumerate(ranked, 1):
|
|
173
|
+
entry["rank_in_lane"] = position
|
|
174
|
+
for entry in scorecard:
|
|
175
|
+
entry.setdefault("rank_in_lane", None)
|
|
176
|
+
|
|
177
|
+
savings: List[Dict[str, Any]] = []
|
|
178
|
+
for lane in {c["lane"] for c in scorecard}:
|
|
179
|
+
eligible = [c for c in scorecard if c["lane"] == lane and c["rankable"]
|
|
180
|
+
and c["median_cost_per_kg"] is not None and c["total_kg"] > 0]
|
|
181
|
+
for current in eligible:
|
|
182
|
+
for alternative in eligible:
|
|
183
|
+
if alternative is current:
|
|
184
|
+
continue
|
|
185
|
+
if (alternative["median_cost_per_kg"] < current["median_cost_per_kg"]
|
|
186
|
+
and alternative["on_time_lower_bound"] >= current["on_time_lower_bound"]):
|
|
187
|
+
estimate = current["total_kg"] * (current["median_cost_per_kg"] - alternative["median_cost_per_kg"])
|
|
188
|
+
savings.append({
|
|
189
|
+
"lane": lane,
|
|
190
|
+
"from_carrier": current["carrier"],
|
|
191
|
+
"to_carrier": alternative["carrier"],
|
|
192
|
+
"volume_kg": current["total_kg"],
|
|
193
|
+
"rate_difference_per_kg": round(current["median_cost_per_kg"] - alternative["median_cost_per_kg"], 4),
|
|
194
|
+
"estimated_saving": round(estimate, 2),
|
|
195
|
+
})
|
|
196
|
+
# Keep the best alternative per (lane, from_carrier).
|
|
197
|
+
best: Dict[tuple[str, str], Dict[str, Any]] = {}
|
|
198
|
+
for item in savings:
|
|
199
|
+
key = (item["lane"], item["from_carrier"])
|
|
200
|
+
if key not in best or item["estimated_saving"] > best[key]["estimated_saving"]:
|
|
201
|
+
best[key] = item
|
|
202
|
+
savings = sorted(best.values(), key=lambda s: -s["estimated_saving"])
|
|
203
|
+
|
|
204
|
+
scorecard.sort(key=lambda c: (c["lane"], c["rank_in_lane"] or math.inf, c["carrier"]))
|
|
205
|
+
assumptions = [
|
|
206
|
+
f"carriers ranked within each lane by the {confidence:.0%} Wilson lower bound of on-time rate, then median cost/kg",
|
|
207
|
+
f"only carrier-lane pairs with ≥ {min_shipments} shipments are ranked or used for savings",
|
|
208
|
+
"cost per kg uses billed weight where present, otherwise actual weight",
|
|
209
|
+
f"cost/kg anomaly: modified z-score > {ANOMALY_THRESHOLD} within the lane (lanes with ≥ {MIN_LANE_ROWS_FOR_OUTLIERS} weighed shipments); "
|
|
210
|
+
"MAD-based, falling back to 1.253314 × mean absolute deviation when MAD is 0",
|
|
211
|
+
f"weight anomaly: billed weight > {OVERBILLED_WEIGHT_RATIO:g} × actual",
|
|
212
|
+
"savings require the alternative to be cheaper per kg and at least as reliable; "
|
|
213
|
+
"estimate = volume × rate difference, excluding contract terms, surcharges, minimums and capacity",
|
|
214
|
+
]
|
|
215
|
+
if scope["mode"] == "all_owners":
|
|
216
|
+
assumptions.insert(0, scope["marker"])
|
|
217
|
+
|
|
218
|
+
total_saving = round(sum(s["estimated_saving"] for s in savings), 2)
|
|
219
|
+
summary = (
|
|
220
|
+
f"{len(records)} waybills across {len({c['lane'] for c in scorecard})} lanes and "
|
|
221
|
+
f"{len({c['carrier'] for c in scorecard})} carriers ({source}); {_count(len(anomalies), 'anomaly', 'anomalies')} "
|
|
222
|
+
f"to check; {_count(len(savings), 'savings opportunity', 'savings opportunities')}, "
|
|
223
|
+
f"estimated {total_saving:,.2f} in total."
|
|
224
|
+
)
|
|
225
|
+
return {
|
|
226
|
+
"success": True,
|
|
227
|
+
"data": {"scope": scope, "scorecard": scorecard, "anomalies": anomalies,
|
|
228
|
+
"savings": savings, "estimated_total_saving": total_saving,
|
|
229
|
+
"assumptions": assumptions},
|
|
230
|
+
"source": source,
|
|
231
|
+
"summary": summary,
|
|
232
|
+
}
|
|
233
|
+
except (OSError, ValueError, TypeError, TenancyError) as exc:
|
|
234
|
+
return {"success": False, "error": str(exc)}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
SCHEMA = {
|
|
238
|
+
"name": "score_carriers",
|
|
239
|
+
"description": (
|
|
240
|
+
"Carrier scorecard by lane (on-time rate with a confidence lower bound, median cost/kg, "
|
|
241
|
+
"exception rate), freight cost anomalies and like-for-like savings, from supplied "
|
|
242
|
+
"waybills. Refuses to mix shippers' data unless told which one."
|
|
243
|
+
),
|
|
244
|
+
"parameters": {
|
|
245
|
+
"type": "object",
|
|
246
|
+
"properties": {
|
|
247
|
+
"file_path": {"type": "string", "description": "CSV or JSON file of waybills"},
|
|
248
|
+
"waybills": {"type": "array", "description": "Waybills: carrier, total_cost, origin, destination, billed_weight_kg, is_on_time"},
|
|
249
|
+
"min_shipments": {"type": "integer", "description": "Minimum shipments for a carrier-lane to be ranked, default 20"},
|
|
250
|
+
**owner_params_schema(),
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Auditable freight analysis from supplied waybills or an existing local database."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import json
|
|
7
|
+
import math
|
|
8
|
+
import pathlib
|
|
9
|
+
import sqlite3
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from contextlib import closing
|
|
12
|
+
from typing import Any, Dict, List
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _number(record: dict[str, Any], field: str, row: int, *, required: bool = False) -> float | None:
|
|
16
|
+
value = record.get(field)
|
|
17
|
+
if value is None or value == "":
|
|
18
|
+
if required:
|
|
19
|
+
raise ValueError(f"Row {row}: {field} is required")
|
|
20
|
+
return None
|
|
21
|
+
if isinstance(value, bool):
|
|
22
|
+
raise ValueError(f"Row {row}: {field} must be a number")
|
|
23
|
+
try:
|
|
24
|
+
number = float(value)
|
|
25
|
+
except (TypeError, ValueError) as exc:
|
|
26
|
+
raise ValueError(f"Row {row}: {field} must be a number") from exc
|
|
27
|
+
if not math.isfinite(number) or number < 0:
|
|
28
|
+
raise ValueError(f"Row {row}: {field} must be finite and non-negative")
|
|
29
|
+
return number
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _on_time(value: Any, row: int) -> bool | None:
|
|
33
|
+
if value is None or value == "":
|
|
34
|
+
return None
|
|
35
|
+
if isinstance(value, bool):
|
|
36
|
+
return value
|
|
37
|
+
if value in (0, 1):
|
|
38
|
+
return bool(value)
|
|
39
|
+
if isinstance(value, str):
|
|
40
|
+
normalized = value.strip().lower()
|
|
41
|
+
if normalized in {"true", "yes", "1"}:
|
|
42
|
+
return True
|
|
43
|
+
if normalized in {"false", "no", "0"}:
|
|
44
|
+
return False
|
|
45
|
+
raise ValueError(f"Row {row}: is_on_time must be a boolean")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def tool_analyze_logistics_data(params: Dict[str, Any]) -> Dict[str, Any]:
|
|
49
|
+
"""Analyze actual records. Empty input never produces representative data."""
|
|
50
|
+
try:
|
|
51
|
+
file_path = params.get("file_path")
|
|
52
|
+
records = params.get("waybills")
|
|
53
|
+
if file_path and records is not None:
|
|
54
|
+
raise ValueError("Provide either file_path or waybills, not both")
|
|
55
|
+
if file_path:
|
|
56
|
+
path = pathlib.Path(file_path).expanduser().resolve()
|
|
57
|
+
if path.suffix.lower() == ".json":
|
|
58
|
+
content = json.loads(path.read_text(encoding="utf-8"))
|
|
59
|
+
records = content if isinstance(content, list) else content.get("waybills") if isinstance(content, dict) else None
|
|
60
|
+
elif path.suffix.lower() == ".csv":
|
|
61
|
+
with path.open(encoding="utf-8-sig", newline="") as handle:
|
|
62
|
+
records = list(csv.DictReader(handle))
|
|
63
|
+
else:
|
|
64
|
+
raise ValueError("Only CSV and JSON waybill files are supported")
|
|
65
|
+
source = str(path)
|
|
66
|
+
data_source = f"file:{path.name}"
|
|
67
|
+
elif records is None:
|
|
68
|
+
path = pathlib.Path.home() / ".aria" / "erp_warehouse.db"
|
|
69
|
+
if not path.is_file():
|
|
70
|
+
raise ValueError("No waybills supplied and no local logistics database found")
|
|
71
|
+
with closing(sqlite3.connect(f"{path.as_uri()}?mode=ro", uri=True)) as connection:
|
|
72
|
+
connection.row_factory = sqlite3.Row
|
|
73
|
+
records = [dict(row) for row in connection.execute("SELECT * FROM logistics_waybills")]
|
|
74
|
+
source = str(path)
|
|
75
|
+
data_source = "local logistics database"
|
|
76
|
+
else:
|
|
77
|
+
source = "provided_waybills"
|
|
78
|
+
data_source = "caller-supplied records"
|
|
79
|
+
|
|
80
|
+
if not isinstance(records, list) or not records:
|
|
81
|
+
raise ValueError("No waybill records available for analysis")
|
|
82
|
+
|
|
83
|
+
# Before this, every shipper's waybills were summed into one spend
|
|
84
|
+
# figure and one carrier table — a 3PL report built from all its
|
|
85
|
+
# clients' commercial data. See logistics_tenancy for the rules.
|
|
86
|
+
from .tenancy import scope_records
|
|
87
|
+
records, scope = scope_records(records, params.get("owner_id"),
|
|
88
|
+
all_owners=bool(params.get("all_owners")))
|
|
89
|
+
|
|
90
|
+
spend = 0.0
|
|
91
|
+
known_delivery = 0
|
|
92
|
+
on_time = 0
|
|
93
|
+
carriers: dict[str, dict[str, Any]] = defaultdict(lambda: {"count": 0, "total_spend": 0.0, "known_delivery_count": 0, "on_time_count": 0})
|
|
94
|
+
anomalies: list[dict[str, Any]] = []
|
|
95
|
+
for row, record in enumerate(records, 1):
|
|
96
|
+
if not isinstance(record, dict):
|
|
97
|
+
raise ValueError(f"Row {row}: waybill must be an object")
|
|
98
|
+
cost = _number(record, "total_cost", row, required=True)
|
|
99
|
+
assert cost is not None
|
|
100
|
+
actual = _number(record, "actual_weight_kg", row)
|
|
101
|
+
billed = _number(record, "billed_weight_kg", row)
|
|
102
|
+
timely = _on_time(record.get("is_on_time"), row)
|
|
103
|
+
carrier = str(record.get("carrier") or "Unknown")
|
|
104
|
+
spend += cost
|
|
105
|
+
carriers[carrier]["count"] += 1
|
|
106
|
+
carriers[carrier]["total_spend"] += cost
|
|
107
|
+
if timely is not None:
|
|
108
|
+
known_delivery += 1
|
|
109
|
+
on_time += int(timely)
|
|
110
|
+
carriers[carrier]["known_delivery_count"] += 1
|
|
111
|
+
carriers[carrier]["on_time_count"] += int(timely)
|
|
112
|
+
if actual is not None and billed is not None and actual > 0 and billed > actual * 1.2:
|
|
113
|
+
anomalies.append({"waybill_no": record.get("waybill_no"), "carrier": carrier, "actual_weight_kg": actual, "billed_weight_kg": billed, "reason": "Billed weight exceeds actual weight by more than 20%; verify dimensional weight before disputing"})
|
|
114
|
+
|
|
115
|
+
metrics = []
|
|
116
|
+
for carrier, item in sorted(carriers.items()):
|
|
117
|
+
item["total_spend"] = round(item["total_spend"], 2)
|
|
118
|
+
item["on_time_rate"] = round(item["on_time_count"] / item["known_delivery_count"] * 100, 2) if item["known_delivery_count"] else None
|
|
119
|
+
metrics.append({"carrier": carrier, **item})
|
|
120
|
+
rate = round(on_time / known_delivery * 100, 2) if known_delivery else None
|
|
121
|
+
result = {"scope": scope, "total_waybills": len(records), "total_freight_spend": round(spend, 2), "overall_on_time_rate": rate, "known_delivery_count": known_delivery, "carrier_metrics": metrics, "billing_anomalies": anomalies, "data_source": data_source}
|
|
122
|
+
rate_label = f"{rate}%" if rate is not None else "不可得(无准时状态记录)"
|
|
123
|
+
return {"success": True, "data": result, "source": source, "summary": f"已审计 {len(records)} 单运单(来源:{data_source}),运费总计 ¥{spend:,.2f},已知准时交付率 {rate_label},发现 {len(anomalies)} 笔需核实的计费重量异常。"}
|
|
124
|
+
except (OSError, sqlite3.Error, ValueError, TypeError, json.JSONDecodeError) as exc:
|
|
125
|
+
# TenancyError is a ValueError, so a refusal to mix shippers lands here.
|
|
126
|
+
return {"success": False, "error": str(exc)}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def register_logistics_tools(tools_dict: Dict[str, Any], schemas_list: List[Dict[str, Any]]) -> int:
|
|
130
|
+
"""Register the logistics analyses as local tools."""
|
|
131
|
+
from .tenancy import owner_params_schema
|
|
132
|
+
|
|
133
|
+
tools_dict["analyze_logistics_data"] = (tool_analyze_logistics_data, "Analyze supplied freight waybills and billing anomalies")
|
|
134
|
+
schemas_list.append({
|
|
135
|
+
"name": "analyze_logistics_data",
|
|
136
|
+
"description": "Analyze actual waybills from supplied records, a CSV/JSON file, or an existing local database",
|
|
137
|
+
"parameters": {
|
|
138
|
+
"type": "object",
|
|
139
|
+
"properties": {
|
|
140
|
+
"file_path": {"type": "string", "description": "CSV or JSON waybill file path"},
|
|
141
|
+
"waybills": {"type": "array", "description": "Waybill records with total_cost"},
|
|
142
|
+
**owner_params_schema(),
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
from .carriers import SCHEMA as _carrier_schema, tool_score_carriers
|
|
148
|
+
from .inventory import SCHEMA as _inventory_schema, tool_plan_inventory_policy
|
|
149
|
+
|
|
150
|
+
tools_dict["plan_inventory_policy"] = (tool_plan_inventory_policy, _inventory_schema["description"])
|
|
151
|
+
schemas_list.append(_inventory_schema)
|
|
152
|
+
tools_dict["score_carriers"] = (tool_score_carriers, _carrier_schema["description"])
|
|
153
|
+
schemas_list.append(_carrier_schema)
|
|
154
|
+
return 3
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Inventory policy for a 3PL: when to reorder, how much, and what is stuck.
|
|
2
|
+
|
|
3
|
+
Everything here is a standard, published formula, applied to the records the
|
|
4
|
+
caller supplies, with the formula and every threshold written into the result
|
|
5
|
+
so a client can recompute any number by hand:
|
|
6
|
+
|
|
7
|
+
z = Φ⁻¹(service level)
|
|
8
|
+
safety stock = z · √(L·σ_d² + d̄²·σ_L²) demand and lead-time variability
|
|
9
|
+
reorder point= d̄·L + safety stock
|
|
10
|
+
order-up-to = reorder point + d̄·R R = review period
|
|
11
|
+
order qty = order-up-to − inventory position, when position ≤ reorder point
|
|
12
|
+
position = on hand + on order − backordered
|
|
13
|
+
|
|
14
|
+
ABC ranks a shipper's SKUs by annual consumption value (Pareto 80/95), XYZ by
|
|
15
|
+
the coefficient of variation of daily demand (0.5 / 1.0). Movement status
|
|
16
|
+
comes from days since last movement (30 slow, 90 dead).
|
|
17
|
+
|
|
18
|
+
What it will not do is guess. A SKU with less demand history than
|
|
19
|
+
`min_history_days` gets no safety stock and no reorder point — it is reported
|
|
20
|
+
as insufficient history, because a policy computed from a handful of days
|
|
21
|
+
would be confidently wrong, and a reorder quantity is money.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import math
|
|
27
|
+
import statistics
|
|
28
|
+
from collections import defaultdict
|
|
29
|
+
from statistics import NormalDist
|
|
30
|
+
from typing import Any, Dict, List
|
|
31
|
+
|
|
32
|
+
from .records import load_records, number, series, text
|
|
33
|
+
from .tenancy import OWNER_FIELD, TenancyError, owner_params_schema, scope_records
|
|
34
|
+
|
|
35
|
+
DEFAULTS = {
|
|
36
|
+
"service_level": 0.95,
|
|
37
|
+
"review_period_days": 7.0,
|
|
38
|
+
"min_history_days": 14,
|
|
39
|
+
"slow_days": 30.0,
|
|
40
|
+
"dead_days": 90.0,
|
|
41
|
+
"excess_cover_days": 180.0,
|
|
42
|
+
}
|
|
43
|
+
ABC_BOUNDS = (0.80, 0.95)
|
|
44
|
+
XYZ_BOUNDS = (0.5, 1.0)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _setting(params: Dict[str, Any], key: str) -> float:
|
|
48
|
+
value = params.get(key, DEFAULTS[key])
|
|
49
|
+
try:
|
|
50
|
+
value = float(value)
|
|
51
|
+
except (TypeError, ValueError) as exc:
|
|
52
|
+
raise ValueError(f"{key} must be a number") from exc
|
|
53
|
+
if not math.isfinite(value) or value < 0:
|
|
54
|
+
raise ValueError(f"{key} must be finite and non-negative")
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _demand(record: Dict[str, Any], row: int) -> tuple[float, float, int] | None:
|
|
59
|
+
"""(mean, sample std, days of history), from a daily series or from aggregates."""
|
|
60
|
+
history = series(record, "daily_demand", row)
|
|
61
|
+
if history is not None:
|
|
62
|
+
if len(history) < 2:
|
|
63
|
+
return (history[0] if history else 0.0), 0.0, len(history)
|
|
64
|
+
return statistics.fmean(history), statistics.stdev(history), len(history)
|
|
65
|
+
mean = number(record, "demand_mean", row)
|
|
66
|
+
if mean is None:
|
|
67
|
+
return None
|
|
68
|
+
std = number(record, "demand_std", row) or 0.0
|
|
69
|
+
days = number(record, "demand_days", row)
|
|
70
|
+
return mean, std, int(days) if days is not None else 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _abc(items: List[Dict[str, Any]]) -> None:
|
|
74
|
+
"""Assign ABC within each shipper's portfolio, by annual consumption value."""
|
|
75
|
+
by_owner: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
|
76
|
+
for item in items:
|
|
77
|
+
if item["annual_value"] is not None:
|
|
78
|
+
by_owner[item.get(OWNER_FIELD) or ""].append(item)
|
|
79
|
+
for group in by_owner.values():
|
|
80
|
+
total = sum(i["annual_value"] for i in group)
|
|
81
|
+
if total <= 0:
|
|
82
|
+
continue
|
|
83
|
+
cumulative = 0.0
|
|
84
|
+
for item in sorted(group, key=lambda i: (-i["annual_value"], i["sku"])):
|
|
85
|
+
share_before = cumulative / total
|
|
86
|
+
item["abc"] = "A" if share_before < ABC_BOUNDS[0] else "B" if share_before < ABC_BOUNDS[1] else "C"
|
|
87
|
+
cumulative += item["annual_value"]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def tool_plan_inventory_policy(params: Dict[str, Any]) -> Dict[str, Any]:
|
|
91
|
+
try:
|
|
92
|
+
records, source = load_records(params, "skus")
|
|
93
|
+
records, scope = scope_records(records, params.get("owner_id"),
|
|
94
|
+
all_owners=bool(params.get("all_owners")))
|
|
95
|
+
|
|
96
|
+
service_level = _setting(params, "service_level")
|
|
97
|
+
if not 0.5 <= service_level < 1:
|
|
98
|
+
raise ValueError("service_level must be at least 0.5 and below 1")
|
|
99
|
+
review = _setting(params, "review_period_days")
|
|
100
|
+
min_history = int(_setting(params, "min_history_days"))
|
|
101
|
+
slow_days = _setting(params, "slow_days")
|
|
102
|
+
dead_days = _setting(params, "dead_days")
|
|
103
|
+
excess_days = _setting(params, "excess_cover_days")
|
|
104
|
+
z = NormalDist().inv_cdf(service_level)
|
|
105
|
+
|
|
106
|
+
items: List[Dict[str, Any]] = []
|
|
107
|
+
for row, record in enumerate(records, 1):
|
|
108
|
+
sku = text(record, "sku", row, required=True)
|
|
109
|
+
on_hand = number(record, "on_hand", row, required=True)
|
|
110
|
+
on_order = number(record, "on_order", row) or 0.0
|
|
111
|
+
backorder = number(record, "backorder", row) or 0.0
|
|
112
|
+
lead = number(record, "lead_time_days", row, required=True, positive=True)
|
|
113
|
+
lead_std = number(record, "lead_time_std_days", row) or 0.0
|
|
114
|
+
unit_cost = number(record, "unit_cost", row)
|
|
115
|
+
idle_days = number(record, "days_since_last_movement", row)
|
|
116
|
+
demand = _demand(record, row)
|
|
117
|
+
if demand is None:
|
|
118
|
+
raise ValueError(f"Row {row}: daily_demand (or demand_mean) is required")
|
|
119
|
+
mean, std, history_days = demand
|
|
120
|
+
position = on_hand + on_order - backorder
|
|
121
|
+
|
|
122
|
+
item: Dict[str, Any] = {
|
|
123
|
+
OWNER_FIELD: text(record, OWNER_FIELD, row) or None,
|
|
124
|
+
"warehouse_id": text(record, "warehouse_id", row) or None,
|
|
125
|
+
"sku": sku,
|
|
126
|
+
"on_hand": on_hand,
|
|
127
|
+
"inventory_position": round(position, 2),
|
|
128
|
+
"avg_daily_demand": round(mean, 4),
|
|
129
|
+
"demand_std": round(std, 4),
|
|
130
|
+
"history_days": history_days,
|
|
131
|
+
"lead_time_days": lead,
|
|
132
|
+
"safety_stock": None,
|
|
133
|
+
"reorder_point": None,
|
|
134
|
+
"order_up_to": None,
|
|
135
|
+
"suggested_order_qty": 0,
|
|
136
|
+
"days_of_cover": round(on_hand / mean, 1) if mean > 0 else None,
|
|
137
|
+
"abc": None,
|
|
138
|
+
"xyz": None,
|
|
139
|
+
"movement": None,
|
|
140
|
+
"flags": [],
|
|
141
|
+
"annual_value": round(mean * 365 * unit_cost, 2) if unit_cost is not None else None,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if mean > 0:
|
|
145
|
+
cv = std / mean
|
|
146
|
+
item["xyz"] = "X" if cv <= XYZ_BOUNDS[0] else "Y" if cv <= XYZ_BOUNDS[1] else "Z"
|
|
147
|
+
|
|
148
|
+
if idle_days is not None:
|
|
149
|
+
item["movement"] = ("dead" if idle_days >= dead_days
|
|
150
|
+
else "slow" if idle_days >= slow_days else "active")
|
|
151
|
+
|
|
152
|
+
if mean == 0:
|
|
153
|
+
item["action"] = "no_demand"
|
|
154
|
+
if on_hand > 0:
|
|
155
|
+
item["flags"].append("stock held with no demand in the history")
|
|
156
|
+
elif history_days < min_history:
|
|
157
|
+
item["action"] = "insufficient_history"
|
|
158
|
+
item["flags"].append(
|
|
159
|
+
f"{history_days} days of demand history; at least {min_history} are needed "
|
|
160
|
+
f"for a safety stock that means anything"
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
safety = z * math.sqrt(lead * std ** 2 + mean ** 2 * lead_std ** 2)
|
|
164
|
+
reorder_point = mean * lead + safety
|
|
165
|
+
order_up_to = reorder_point + mean * review
|
|
166
|
+
# Rounded up: a fractional unit cannot be held, and rounding the
|
|
167
|
+
# buffer down would quietly lower the service level.
|
|
168
|
+
item["safety_stock"] = math.ceil(safety)
|
|
169
|
+
item["reorder_point"] = math.ceil(reorder_point)
|
|
170
|
+
item["order_up_to"] = math.ceil(order_up_to)
|
|
171
|
+
if position <= reorder_point:
|
|
172
|
+
item["action"] = "reorder"
|
|
173
|
+
item["suggested_order_qty"] = max(0, math.ceil(order_up_to - position))
|
|
174
|
+
else:
|
|
175
|
+
item["action"] = "ok"
|
|
176
|
+
|
|
177
|
+
if item["days_of_cover"] is not None and item["days_of_cover"] > excess_days:
|
|
178
|
+
item["flags"].append(f"{item['days_of_cover']:g} days of cover exceeds {excess_days:g}")
|
|
179
|
+
items.append(item)
|
|
180
|
+
|
|
181
|
+
_abc(items)
|
|
182
|
+
|
|
183
|
+
order = {"reorder": 0, "insufficient_history": 1, "no_demand": 2, "ok": 3}
|
|
184
|
+
items.sort(key=lambda i: (order[i["action"]],
|
|
185
|
+
i["days_of_cover"] if i["days_of_cover"] is not None else math.inf,
|
|
186
|
+
i["sku"]))
|
|
187
|
+
|
|
188
|
+
counts = defaultdict(int)
|
|
189
|
+
for item in items:
|
|
190
|
+
counts[item["action"]] += 1
|
|
191
|
+
dead = sum(1 for i in items if i["movement"] == "dead")
|
|
192
|
+
slow = sum(1 for i in items if i["movement"] == "slow")
|
|
193
|
+
|
|
194
|
+
assumptions = [
|
|
195
|
+
f"service level {service_level:.1%} → z = {z:.3f}",
|
|
196
|
+
"safety stock = z·√(L·σ_d² + d̄²·σ_L²); reorder point = d̄·L + safety stock",
|
|
197
|
+
f"order-up-to = reorder point + d̄·{review:g} (review period, days)",
|
|
198
|
+
"inventory position = on hand + on order − backordered",
|
|
199
|
+
"safety stock, reorder point and order quantity rounded up to whole units",
|
|
200
|
+
f"no policy below {min_history} days of demand history",
|
|
201
|
+
f"ABC by annual consumption value within each shipper (A < {ABC_BOUNDS[0]:.0%}, B < {ABC_BOUNDS[1]:.0%} cumulative)",
|
|
202
|
+
f"XYZ by demand CV (X ≤ {XYZ_BOUNDS[0]}, Y ≤ {XYZ_BOUNDS[1]}, Z above)",
|
|
203
|
+
f"movement: slow ≥ {slow_days:g} days idle, dead ≥ {dead_days:g}",
|
|
204
|
+
"capacity, MOQs, case packs and supplier terms are not modelled",
|
|
205
|
+
]
|
|
206
|
+
if scope["mode"] == "all_owners":
|
|
207
|
+
assumptions.insert(0, scope["marker"])
|
|
208
|
+
|
|
209
|
+
summary = (
|
|
210
|
+
f"{len(items)} {'SKU' if len(items) == 1 else 'SKUs'} analysed ({source}): {counts['reorder']} to reorder, "
|
|
211
|
+
f"{counts['insufficient_history']} with too little history, {counts['no_demand']} "
|
|
212
|
+
f"with no demand; {dead} dead and {slow} slow-moving."
|
|
213
|
+
)
|
|
214
|
+
return {
|
|
215
|
+
"success": True,
|
|
216
|
+
"data": {"scope": scope, "items": items, "counts": dict(counts),
|
|
217
|
+
"dead_stock": dead, "slow_stock": slow, "assumptions": assumptions},
|
|
218
|
+
"source": source,
|
|
219
|
+
"summary": summary,
|
|
220
|
+
}
|
|
221
|
+
except (OSError, ValueError, TypeError, TenancyError) as exc:
|
|
222
|
+
return {"success": False, "error": str(exc)}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
SCHEMA = {
|
|
226
|
+
"name": "plan_inventory_policy",
|
|
227
|
+
"description": (
|
|
228
|
+
"Reorder points, safety stock, order quantities, ABC/XYZ classes and dead/slow stock "
|
|
229
|
+
"for a shipper's SKUs, from supplied demand history and lead times. Formulas are "
|
|
230
|
+
"stated in the result. Refuses to mix shippers' data unless told which one."
|
|
231
|
+
),
|
|
232
|
+
"parameters": {
|
|
233
|
+
"type": "object",
|
|
234
|
+
"properties": {
|
|
235
|
+
"file_path": {"type": "string", "description": "CSV or JSON file of SKU records"},
|
|
236
|
+
"skus": {"type": "array", "description": "SKU records: sku, on_hand, lead_time_days, daily_demand"},
|
|
237
|
+
"service_level": {"type": "number", "description": "Target cycle service level, default 0.95"},
|
|
238
|
+
"review_period_days": {"type": "number", "description": "Days between reviews, default 7"},
|
|
239
|
+
**owner_params_schema(),
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
}
|