diff --git a/.cursor/tmp_test_upload.pdf b/.cursor/tmp_test_upload.pdf new file mode 100644 index 000000000..2995a4d0e Binary files /dev/null and b/.cursor/tmp_test_upload.pdf differ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..73f9732e7 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @Vector897 diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..9c949236f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,32 @@ +name: Bug report +description: Report a reproducible problem in Globot. +title: "[Bug]: " +labels: [bug, needs-triage] +body: + - type: markdown + attributes: + value: "Thanks for helping improve Globot. Do not include credentials, personal data, or security vulnerabilities." + - type: textarea + id: summary + attributes: + label: What happened? + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: Steps to reproduce + placeholder: "1. ...\n2. ...\n3. ..." + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behaviour + validations: + required: true + - type: input + id: environment + attributes: + label: Environment + placeholder: "OS, Python/Node version, browser, commit SHA" diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..316386439 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Security vulnerability + url: https://github.com/Vector897/Globot/blob/main/SECURITY.md + about: Do not open a public issue for security reports. Follow the security policy. + - name: Support + url: https://github.com/Vector897/Globot/blob/main/SUPPORT.md + about: How to ask questions and get help. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..fe5c359f0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,21 @@ +name: Feature request +description: Propose an improvement to Globot. +title: "[Feature]: " +labels: [enhancement, needs-triage] +body: + - type: textarea + id: problem + attributes: + label: What problem would this solve? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed approach + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..2aa87f366 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: /backend + schedule: + interval: weekly + - package-ecosystem: npm + directory: /frontend + schedule: + interval: weekly + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..87586ff4b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,14 @@ +## Summary + +Describe the problem and the change. + +## Validation + +- [ ] `python -m compileall -q backend` +- [ ] `cd frontend && pnpm build` +- [ ] Documentation updated where needed + +## Safety and data + +- [ ] No credentials, personal data, or private operational data were added. +- [ ] Human review remains required for consequential recommendations. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..66d9d68d9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + backend-syntax: + name: Backend syntax + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Compile Python sources + run: python -m compileall -q backend + + frontend-build: + name: Frontend build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 11 + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: pnpm + cache-dependency-path: frontend/pnpm-lock.yaml + - name: Install dependencies + working-directory: frontend + run: pnpm install --frozen-lockfile + - name: Build + working-directory: frontend + run: pnpm build + env: + VITE_CLERK_PUBLISHABLE_KEY: pk_test_ci_placeholder + VITE_API_BASE_URL: http://localhost:8000 diff --git a/.gitignore b/.gitignore index 5c03b089a..3bcbe650a 100644 --- a/.gitignore +++ b/.gitignore @@ -292,11 +292,3 @@ site/ # 如果需要忽略自动生成的迁移文件,取消注释: # backend/alembic/versions/*.py # 但通常建议保留迁移文件 - -# Ignore all markdown files except README.md -*.md -!README.md - - - -.txt \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..bfdf1b183 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,21 @@ +# Agent contribution guidance + +Automated coding agents working in this repository must follow the same standards as human contributors. + +## Scope and safety + +- Prefer small, reviewable changes with tests or a documented validation step. +- Do not add credentials, tokens, personal data, private URLs, local paths, or generated databases to commits. +- Treat model outputs, financial calculations, compliance assessments, and route recommendations as advisory. Do not remove human review or approval controls. +- Do not claim that mock data is live, verified, or production-ready. + +## Required checks + +Run the checks that cover your change before proposing it: + +```bash +python -m compileall -q backend +cd frontend && pnpm build +``` + +Update `README.md` and the relevant documentation whenever setup, configuration, data assumptions, or user-visible behaviour changes. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..e9021d31f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +All notable changes to Globot will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project aims to follow [Semantic Versioning](https://semver.org/). + +## [Unreleased] + +### Added + +- MIT `LICENSE` and community health files (Contributing, Security, Code of Conduct, Governance, Support). +- CI workflow (backend syntax + frontend production build) and Dependabot updates. +- Maintainer-oriented OpenAI/Codex plan and public roadmap under `docs/`. +- Backend / frontend `.env.example` files with safe defaults. +- Typed decision-record schemas for human-reviewed recommendations. + +### Changed + +- Rewrote the README as an OSS decision-support project (badges, architecture, clear mock-data boundaries). +- Removed personal administrator defaults; debug mode is off by default. +- Frontend package renamed to `globot-frontend`; Docker base image aligned to Python 3.11. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..df61be86e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,23 @@ +# Contributor Covenant Code of Conduct + +## Our pledge + +We pledge to make participation in the Globot community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +## Our standards + +Examples of behaviour that contributes to a positive environment include: + +- demonstrating empathy and kindness toward other people; +- respecting differing opinions, viewpoints, and experiences; +- giving and gracefully accepting constructive feedback; +- accepting responsibility and apologising to those affected by mistakes; and +- focusing on what is best for the overall community. + +Examples of unacceptable behaviour include harassment, sexualised language or imagery, personal or political attacks, public or private harassment, publishing others' private information without permission, and other conduct that could reasonably be considered inappropriate in a professional setting. + +## Enforcement + +Community leaders are responsible for clarifying and enforcing these standards. Reports may be made privately through the repository owner's GitHub profile. All complaints will be reviewed and investigated promptly and fairly. Leaders may remove, edit, or reject comments, commits, code, issues, and other contributions that are not aligned with this Code of Conduct. + +This Code of Conduct is adapted from the [Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..89780240a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Contributing to Globot + +Thanks for helping improve Globot. Contributions are welcome in code, documentation, tests, demo scenarios, and design review. + +## Before you begin + +1. Search existing issues and pull requests to avoid duplicate work. +2. For a material change, open an issue first and describe the user problem, proposed approach, and validation plan. +3. Do not include credentials, personal data, confidential shipping records, or licensed data that cannot be redistributed. + +## Development workflow + +1. Fork the repository and create a focused branch from `main`. +2. Keep the change small and explain the behaviour it changes. +3. Update relevant documentation and examples in the same pull request. +4. Run the checks below before requesting review: + + ```bash + python -m compileall -q backend + cd frontend && pnpm build + ``` + +5. In the pull request, state the motivation, how you tested it, and any follow-up work. + +## Pull-request expectations + +- Use clear, descriptive commit and pull-request titles. +- Include tests when changing behaviour; explain why when tests are not practical. +- Keep generated files, local databases, uploads, and secrets out of commits. +- Preserve the human-review boundary for recommendations that could affect logistics, compliance, or financial decisions. +- Be patient and respectful during review. Maintainers may request revisions, defer work, or close changes that do not fit the roadmap. + +## Reporting bugs and proposing features + +Use the GitHub issue templates. A good report includes reproduction steps, expected and actual behaviour, environment details, and a minimal example where possible. Please do not use public issues for security vulnerabilities; follow [SECURITY.md](SECURITY.md) instead. + +By contributing, you agree to follow the [Code of Conduct](CODE_OF_CONDUCT.md). diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 000000000..0bbf89371 --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,21 @@ +# Governance + +## Maintainer + +The repository owner, **Vector897**, is the primary maintainer and is accountable for roadmap decisions, review, releases, security coordination, and community health. + +## How decisions are made + +Issues and pull requests are the public record for project decisions. The maintainer welcomes proposals and will prioritise work that improves reproducibility, security, documentation, test coverage, accessibility, and the project's decision-support mission. + +For a substantive change, the maintainer may ask for an issue, design note, tests, or a staged implementation. Decisions aim for consensus when possible; if consensus is not reached, the primary maintainer makes the final call and records the rationale in the relevant discussion or pull request. + +## Maintainer responsibilities + +- triage issues and review pull requests; +- keep the default branch and release notes understandable; +- protect contributors and reporters handling security issues; +- enforce the [Code of Conduct](CODE_OF_CONDUCT.md); and +- avoid merging features that bypass human review for consequential recommendations. + +Contributors who demonstrate sustained, constructive maintenance work may be invited to take on additional review responsibilities. diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..05e75997b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Vector897 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 76352cb78..dc9a8809e 100644 --- a/README.md +++ b/README.md @@ -1,211 +1,168 @@ -# 🛡️ Globot Shield: Securing Global Lifelines (v2.2.20260201) +# Globot -> **Google Gemini 3 Hackathon 参赛作品 - Powered by Gemini 2.0** +[![CI](https://github.com/Vector897/Globot/actions/workflows/ci.yml/badge.svg)](https://github.com/Vector897/Globot/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![Python 3.11](https://img.shields.io/badge/Python-3.11-3776AB.svg)](backend/requirements.txt) +[![React + Vite](https://img.shields.io/badge/Frontend-React%20%2B%20Vite-61DAFB.svg)](frontend/package.json) -![Globot Dashboard](ai-sales-mvp/frontend/src/assets/dashboard_preview.png) +**Open-source decision-support workspace for supply-chain disruption review.** -## 📖 项目概览 +Globot helps operators inspect disruption signals, compare route options, review compliance documents, and keep a **human accountable** for every consequential recommendation. It is built as a transparent research and demonstration system, not as an autonomous execution engine. -**Globot** 代表了全球贸易风险管理的范式转变。与传统的静态仪表盘不同,Globot 是一个 **Agentic AI (代理智能) 系统**,能够实时主动监控、分析并缓解供应链风险。 +> Default demos use mock and synthetic data. Outputs must be independently reviewed before they inform operational, compliance, or financial decisions. -> 💡 *"We are not just optimizing spreadsheets; we are securing the lifelines of the global economy."* -> -> — 我们不仅仅是在优化表格,我们是在守护全球经济的生命线。 +## Why this project matters -### 🌍 社会价值 (Potential Impact) +Shipping and trade disruptions affect food, energy, medical supply, and manufacturing lead times. Existing tooling is often either a static dashboard or a black-box model. Globot sits in between: -当 Globot 帮助 Maersk 在红海危机中快速改道时,这意味着: -- 🌾 **肯尼亚的粮食**不会断供 -- 🔥 **欧洲的天然气**能够过冬 -- 🏥 **医院的急救设备**不会卡在港口 +- **Inspectable multi-agent activity** — market, logistics, compliance, and hedging perspectives in one review flow +- **Human-in-the-loop by design** — recommendations cannot be treated as actions without explicit approval +- **Reproducible open demo** — local fixtures and documented scenarios so contributors can verify behaviour +- **Pluggable integrations** — mock sources today; production adapters tomorrow (AIS, news, insurance, market data) -此版本 (**v2.1.20260131**) 是专门构建的 **"High-Fidelity Mock (高保真模拟)"** 版本,旨在确保演示过程的绝对稳定和高冲击力。它模拟了一个逼真的 "下午 4:55 危机场景"(霍尔木兹海峡地缘危机),并完整展示了 **AI 思维链 (Chain-of-Thought)** 和 **人机共驾 (Human-in-the-Loop)** 决策过程。 +The repository is an ecosystem building block for **responsible agentic logistics research**: a shared reference for HITL controls, provenance-friendly summaries, and maintenance-friendly FastAPI + React structure. -## 🌟 核心功能 (v2.1 升级版) +## Table of contents -### 1. 🧠 可视化 AI 思维链 (Chain-of-Thought) +- [Current status](#current-status) +- [Core capabilities](#core-capabilities) +- [Quick start](#quick-start) +- [Configuration](#configuration) +- [Architecture](#architecture) +- [Roadmap](#roadmap) +- [Contributing](#contributing) +- [License](#license) -- **实时推理展示**: 像 "打字机" 一样逐行展示 AI 的思考过程,不再是黑盒。 -- **多 Agent 辩论**: 展示 "红队 vs 蓝队" 的对抗性辩论 (Adversarial Debate),确保决策鲁棒性。 -- **引用溯源**: 每个推理步骤都关联到具体的 RAG 知识库文档或实时新闻源。 +## Current status -### 2. 🤖 多 Agent 协作引擎 (5-Agent Reasoning Engine) +Globot is actively maintained as a public research / demonstration project. -展示了由 5 个专业 AI Agent 组成的团队协同工作: +| Area | Status | +| --- | --- | +| Stars / forks | Tracked on GitHub; community PRs welcome | +| License | [MIT](LICENSE) | +| CI | Backend syntax + frontend production build on `main` and PRs | +| Default data | Mock / synthetic — **not** live trading, sanctions, or execution | +| Production readiness | **Not** production-ready without deployers adding auth, secrets, data licenses, and review controls | -- **🔭 市场哨兵 (Market Sentinel)**: 监控路透社/彭博社的地缘政治信号 (Mock API 支持多场景:红海危机、港口拥堵等)。 -- **🛡️ 风险对冲专家 (Financial Hedge Agent)**: 实时分析燃料价格、汇率、运价风险,提供智能对冲策略(期货、期权、远期合约),支持正常与危机时刻的多维度风险管理。动态计算改道后的燃油成本 (+$180K) 和运费波动。 -- **🚢 物流指挥官 (Logistics Orchestrator)**: 重新规划航线以避开冲突区域。 -- **📋 合规经理 (Compliance Manager)**: 使用 **Gemini 2M Token Context Window** 分析 500 页保险条款和制裁名单。 -- **⚖️ 对抗性辩论 (Adversarial Debate)**: 对决策进行红队测试,防止幻觉。 +## Core capabilities -### 3. �️ Visual Risk Intelligence (卫星图像分析) - NEW +1. **Scenario-driven demo** — reproducible disruption walkthrough with route selection and approval steps +2. **Multi-perspective review** — market signals, route risk, compliance document experiments, hedging calculations +3. **Map-centric visualisation** — Deck.gl-based logistics map for ports, routes, and risk highlighting +4. **Human approval gate** — Approve / details / override path before a recommendation is treated as an action +5. **Optional model providers** — Ollama by default; OpenAI and other providers remain opt-in via server-side env vars -利用 **Gemini Vision** 多模态能力: +## Quick start -- **卫星图像分析**: 实时检测港口拥堵、运河堵塞、集装箱堆积。 -- **苏伊士运河场景**: Ever Given 类型事件的早期预警 (官方公告前 6 小时)。 -- **视觉证据**: 在决策中嵌入卫星截图作为推理依据。 +### Prerequisites -### 4. 📄 Long Document Compliance (长文档合规分析) - NEW - -展示 Gemini 长上下文窗口优势: - -- **500 页海事保险条款**自动解析与航线合规校验。 -- **OFAC/UN 制裁名单**实时核查 (2M tokens context)。 -- **MLC 2006 公约**自动验证船员资质。 - -### 5. �🛫 航空级物流全息地图 (Deck.gl) - -- **线路与港口**: 可视化全球主要航线及上海、鹿特丹、洛杉矶等核心港口。 -- **交互式船舶**: 点击地图上的黄色船舶图标,可查看详细货物清单和航行状态。 -- **动态风险**: 危机发生时,受影响区域会高亮并发出脉冲警报。 - -### 6. 👨‍✈️ 人机共驾 (Human-in-the-Loop) - -- **决策确认**: AI 提出建议后,必须由人类点击 **"Approve & Execute"** 才能执行,体现负责任的 AI 原则。 -- **多种选择**: 提供 "Details" (查看详情) 和 "Override" (人工干预) 选项。 - -### 7. 🔒 企业级身份验证与安全 - -- **多渠道登录**: 集成 Clerk,支持 Google, Facebook, LinkedIn 社交登录及邮箱/短信验证码。 -- **管理员控制台**: 专为管理员设计的可视化看板,监控系统全局 KPI。 -- **安全白名单**: 基于环境变量的邮箱白名单系统,确保管理权限的隐私与安全。 - -### 🔌 可插拔架构 (Pluggable Data Sources) - -本项目的 **Reasoning Engine (推理引擎)** 是通用的,当前使用 Mock 数据是 Hackathon 限制。生产环境可直接对接: - -| 数据源 | 用途 | 替换方式 | -| :--- | :--- | :--- | -| Bloomberg Terminal API | 实时市场行情、地缘政治事件 | 替换 `mock_knowledge_base.py` | -| MarineTraffic API | 船舶 AIS 实时定位 | 替换 `demo/cot_data.py` | -| Sentinel-2 Satellite API | 港口/运河卫星图像 | 替换 `visual_risk_service.py` | -| Reuters/Bing News API | 实时新闻流 | 替换 Market Sentinel 数据源 | - -## 🎯 目标客户 (Target Customers) - -| 行业 | 企业示例 | 用户角色 | -| :--- | :--- | :--- | -| 海运与物流 | Maersk, COSCO | NOC Manager, Control Tower Lead | -| 高端制造业 | Tesla, Apple | Global Supply Manager, Resiliency PM | -| 大宗商品交易 | Cargill, Glencore | Commodity Logistics Risk Lead | -| 货运代理 | Flexport | Trade Compliance Officer | - -## 🚀 快速开始 - -### 1. 启动后端 - -#### 前置要求 - Python 3.11 +- Node.js 20+ +- [pnpm](https://pnpm.io/) (lockfile: `frontend/pnpm-lock.yaml`) -#### 安装步骤 +### Backend ```bash -# 进入后端目录 cd backend +python -m venv .venv +# Windows PowerShell +.\.venv\Scripts\Activate.ps1 +# macOS/Linux +# source .venv/bin/activate -# 创建虚拟环境(推荐) -python -m venv venv - -# 激活虚拟环境 -# Windows PowerShell: -.\venv\Scripts\Activate.ps1 -# Windows CMD: - venv\Scripts\activate.bat -# macOS/Linux: - source venv/bin/activate - -# 安装依赖 pip install -r requirements.txt - -# 配置环境变量 -# 在 backend 目录下创建 .env 文件,参考核心配置: -# CLERK_ISSUER_URL=... -# ADMIN_WHITELIST=... - -# 启动服务器 +cp .env.example .env # Windows: copy .env.example .env python start_server.py ``` -_后端运行在 `http://localhost:8000`_ -### 2. 启动前端 +API: `http://localhost:8000` + +### Frontend ```bash cd frontend -npm install +pnpm install --frozen-lockfile +cp .env.example .env # Windows: copy .env.example .env +pnpm dev +``` -# 配置环境变量 -# 在 frontend 目录下创建 .env 文件: -# VITE_CLERK_PUBLISHABLE_KEY=... -# VITE_ADMIN_WHITELIST=... +UI: `http://localhost:5173` -npm run dev -``` -_前端运行在 `http://localhost:5173`_ +### Demo path -### 3. 演示路径 +1. Open `http://localhost:5173/pay` +2. Choose **Watch Demo** → route selection (`/port`) +3. Confirm a route (default Shanghai → Rotterdam) → **Start Simulation** (`/demo`) +4. Follow the activity feed and use **Approve & Execute** only after human review -1. 打开浏览器访问: `http://localhost:5173/pay` -2. 点击 **"Watch Demo"**,跳转至航线选择页 (`/port`)。 -3. 确认路线(默认 Shanghai -> Rotterdam),点击 **"Start Simulation"** 进入演示 (`/demo`)。 -4. 观察 AI 推理过程,待 "Approve & Execute" 按钮出现后点击确认。 +## Configuration -## 📂 关键文件说明 +Copy example env files before local edits. Never commit API keys, Clerk credentials, personal allowlists, uploads, or production data. Defaults contain no credentials and enable no administrator. -- `updates/README.md`: 详细的演示操作步骤说明(新用户必读)。 -- `task.md`: 项目开发任务清单。 -- `updates/README.md`: 服务启动与故障排查速查表。 +| File | Purpose | +| --- | --- | +| `backend/.env.example` | API, optional LLM providers, Clerk issuer, allowlist | +| `frontend/.env.example` | Vite public Clerk key stub and API base URL | -## 💰 Financial Hedging System (NEW) +`backend/data/` holds demonstration assets. Treat all outputs as illustrative unless you replace those sources with licensed, verified data and add operational controls. -Globot now includes a comprehensive financial risk hedging system for managing: +## Architecture -### Risk Categories -- **燃料价格风险 (Fuel Price Risk)**: 使用期货、期权、掉期对冲船用燃料价格波动 -- **汇率风险 (Currency Risk)**: 通过远期合约、货币掉期锁定汇率 -- **运价波动 (Freight Rate Risk)**: 长期租船合同与现货市场组合策略 +```text +React / Vite frontend (review UI + map) + | + v +FastAPI backend + | | | + demo risk/route documents & hedging experiments + | + mock scenarios + local knowledge assets +``` -### Features -- ✅ AI-powered risk assessment with Value at Risk (VaR) calculations -- ✅ Automated hedging strategy recommendations (normal & crisis modes) -- ✅ Real-time market data simulation -- ✅ Crisis detection and emergency hedging protocols -- ✅ Multi-instrument portfolio optimization +Longer-term maintainer automation and opt-in OpenAI usage: [docs/OPENAI_API_PLAN.md](docs/OPENAI_API_PLAN.md). -### API Endpoints -```bash -# Health check -GET http://localhost:8000/api/hedge/health +## Project health -# Get market data -GET http://localhost:8000/api/hedge/market-data +| Signal | Detail | +| --- | --- | +| License | OSI-approved MIT | +| Community files | CONTRIBUTING, SECURITY, CODE_OF_CONDUCT, GOVERNANCE, SUPPORT | +| Automation | GitHub Actions CI + Dependabot | +| Maintainer | Primary maintainer [@Vector897](https://github.com/Vector897) with write access | +| Codex plan | [docs/OPENAI_API_PLAN.md](docs/OPENAI_API_PLAN.md) | -# Assess risk exposure -POST http://localhost:8000/api/hedge/assess-risk +## Roadmap -# Get hedging recommendations -POST http://localhost:8000/api/hedge/recommend +See [docs/ROADMAP.md](docs/ROADMAP.md). Near-term priorities: -# Activate crisis hedging -POST http://localhost:8000/api/hedge/crisis-activate +- Repeatable API and scenario tests in CI +- Explicit, opt-in provider integrations with evaluation fixtures +- Provenance and review records for every UI recommendation +- Maintainer automation for issue triage, PR review summaries, release notes, and dependency hygiene -# Generate executive report -POST http://localhost:8000/api/hedge/report -``` +## Contributing + +Please read [CONTRIBUTING.md](CONTRIBUTING.md) before opening an issue or PR. -### Documentation -- **API Documentation**: `backend/docs/HEDGING_API.md` -- **Strategy Guide**: `backend/docs/HEDGING_STRATEGY_GUIDE.md` -- **Claude Skill**: `backend/claude_skill/financial_hedging/SKILL.md` +| Doc | Purpose | +| --- | --- | +| [SECURITY.md](SECURITY.md) | Vulnerability reporting | +| [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) | Community standards | +| [GOVERNANCE.md](GOVERNANCE.md) | Maintainer decision-making | +| [SUPPORT.md](SUPPORT.md) | How to get help | +| [CHANGELOG.md](CHANGELOG.md) | Notable changes | +| [AGENTS.md](AGENTS.md) | Guidance for automated coding agents | + +Local checks before review: -### Quick Test ```bash -cd backend -python test_hedging_system.py +python -m compileall -q backend +cd frontend && pnpm build ``` ---- +## License + +Globot is released under the [MIT License](LICENSE). -**维护者**: Vector897 -**许可证**: MIT +**Primary maintainer:** [Vector897](https://github.com/Vector897) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..6051cf227 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,22 @@ +# Security policy + +## Supported versions + +Security fixes are applied to the latest `main` branch. Releases are not yet versioned; users of older forks should update to the latest commit before reporting an issue. + +## Reporting a vulnerability + +Please do **not** open a public issue for a suspected vulnerability. Report it privately to the repository owner through the contact method in the GitHub profile, with: + +- a clear description and affected component; +- steps to reproduce or a minimal proof of concept; +- potential impact; and +- any suggested mitigation. + +The maintainer will acknowledge a report within seven days where possible, investigate privately, and coordinate a fix before public disclosure. Please give maintainers reasonable time to remediate before publishing details. + +## Security boundaries + +Globot is a demonstration and research project. It must not be deployed with wildcard cross-origin access, default credentials, personal allowlists, or unreviewed data sources. Deployers are responsible for authentication, least-privilege access, secret management, input validation, dependency updates, logging controls, and review of any model-assisted output. + +Never commit API keys, Clerk credentials, tokens, user uploads, or operational data. If you discover an exposed secret, revoke or rotate it immediately and report it privately. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 000000000..2d1fb7c78 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,8 @@ +# Getting support + +- **Questions and ideas:** open a GitHub Discussion or an issue with the `question` label. +- **Bug reports:** use the bug-report issue template and include reproducible steps. +- **Feature requests:** use the feature-request issue template and describe the problem before proposing a solution. +- **Security vulnerabilities:** follow [SECURITY.md](SECURITY.md); do not disclose them publicly. + +Globot is maintained in available volunteer time. Please provide enough context for another contributor to reproduce the problem and allow reasonable time for a response. diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 000000000..b9c3c8eb3 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,18 @@ +# Application +DATABASE_URL=postgresql://user:password@localhost:5432/globot +LOG_LEVEL=INFO +DEBUG=false + +# Authentication and access control. Leave the allowlist empty for local demo use. +CLERK_ISSUER_URL= +ADMIN_WHITELIST= + +# Optional model providers. Keep all credentials out of source control. +LLM_PROVIDER=ollama +OLLAMA_BASE_URL=http://localhost:11434 +OLLAMA_MODEL=qwen2.5:latest +OPENAI_API_KEY= +OPENAI_BASE_URL= +OPENAI_MODEL=gpt-4o-mini +GOOGLE_API_KEY= +GOOGLE_MAPS_API_KEY= diff --git a/backend/Dockerfile b/backend/Dockerfile index 090ef9f2a..9a044748d 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10-slim +FROM python:3.11-slim WORKDIR /app diff --git a/backend/api/v2/maritime_routes.py b/backend/api/v2/maritime_routes.py index 99efee883..ff00b9389 100644 --- a/backend/api/v2/maritime_routes.py +++ b/backend/api/v2/maritime_routes.py @@ -4,6 +4,7 @@ """ import logging import json +import time from typing import List, Optional from datetime import datetime @@ -35,7 +36,7 @@ logger = logging.getLogger(__name__) -router = APIRouter(prefix="/v2/maritime", tags=["Maritime Compliance"]) +router = APIRouter(prefix="/api/v2/maritime", tags=["Maritime Compliance"]) # ========== Request/Response Models ========== @@ -686,6 +687,28 @@ async def upload_document( Upload a document (certificate, permit) with OCR processing. Supported formats: PDF, PNG, JPG """ + # region agent log + try: + with open("/Users/timothylin/Globot/.cursor/debug.log", "a", encoding="utf-8") as f: + f.write(json.dumps({ + "runId": "pre-fix", + "hypothesisId": "H5", + "location": "backend/api/v2/maritime_routes.py:upload_document:entry", + "message": "upload handler entered", + "data": { + "customerId": customer_id, + "vesselId": vessel_id, + "documentType": document_type.value if hasattr(document_type, "value") else str(document_type), + "titlePresent": bool(title), + "filename": file.filename if file else None, + "contentType": file.content_type if file else None, + }, + "timestamp": int(time.time() * 1000), + }, ensure_ascii=True) + "\n") + except Exception: + pass + # endregion + # Validate vessel exists vessel = db.query(Vessel).filter(Vessel.id == vessel_id).first() if not vessel: diff --git a/backend/api/v2/market_sentinel_routes.py b/backend/api/v2/market_sentinel_routes.py index c22650257..d1504dedb 100644 --- a/backend/api/v2/market_sentinel_routes.py +++ b/backend/api/v2/market_sentinel_routes.py @@ -4,6 +4,8 @@ from datetime import datetime, timedelta import uuid import random +import json +import urllib.request router = APIRouter(prefix="/api/v2/market-sentinel", tags=["market-sentinel"]) @@ -28,6 +30,28 @@ class MarketSentinelResponse(BaseModel): raw_text: str request_echo: Dict[str, Any] + +def _agent_debug_log(hypothesis_id: str, location: str, message: str, data: Dict[str, Any]) -> None: + payload = { + "runId": "pre-fix", + "hypothesisId": hypothesis_id, + "location": location, + "message": message, + "data": data, + "timestamp": int(datetime.utcnow().timestamp() * 1000), + } + try: + req = urllib.request.Request( + "http://127.0.0.1:7242/ingest/05d36e09-cd94-4f96-af55-b3946c76739f", + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(req, timeout=1): + pass + except Exception: + pass + # --- Mock Logic --- def generate_red_sea_crisis_packet(origin, destination): @@ -130,6 +154,20 @@ async def run_analysis(request: MarketSentinelRequest): first_lane = lanes[0] origin = first_lane.get("origin", "Unknown") destination = first_lane.get("destination", "Unknown") + # #region agent log + _agent_debug_log( + "H2", + "backend/api/v2/market_sentinel_routes.py:run_analysis", + "Received Market Sentinel request route context", + { + "lanesCount": len(lanes), + "firstLane": lanes[0] if lanes else None, + "origin": origin, + "destination": destination, + "entitiesCount": len(request.watchlist.get("entities", [])), + }, + ) + # #endregion # 1. Determine Scenario based on Route # Shanghai (CNSHA/CNNGB) -> Rotterdam (NLRTM/DEHAM) = Red Sea Crisis @@ -137,6 +175,20 @@ async def run_analysis(request: MarketSentinelRequest): # Shanghai -> LA/Long Beach (USLAX/USLGB) = Congestion is_us_route = (origin in ["CNSHA", "CNNGB"]) and (destination in ["USLAX", "USLGB", "Los Angeles"]) + # #region agent log + _agent_debug_log( + "H5", + "backend/api/v2/market_sentinel_routes.py:run_analysis", + "Route classification decision", + { + "origin": origin, + "destination": destination, + "is_europe_route": is_europe_route, + "is_us_route": is_us_route, + "selectedBranch": "europe" if is_europe_route else ("us" if is_us_route else "normal"), + }, + ) + # #endregion if is_europe_route: packet = generate_red_sea_crisis_packet(origin, destination) diff --git a/backend/config.py b/backend/config.py index 74f0dd898..82a0a8774 100644 --- a/backend/config.py +++ b/backend/config.py @@ -16,7 +16,7 @@ class Settings(BaseSettings): ) # 数据库 - database_url: str = "postgresql://user:password@localhost:5432/dji_sales_mvp" + database_url: str = "postgresql://user:password@localhost:5432/globot" # LLM选择: ollama 或 openai llm_provider: str = "ollama" @@ -56,11 +56,11 @@ class Settings(BaseSettings): # Clerk配置 clerk_issuer_url: Optional[str] = None - admin_whitelist: str = "thaumatext@gmail.com" + admin_whitelist: str = "" # 系统配置 log_level: str = "INFO" - debug: bool = True + debug: bool = False @lru_cache() diff --git a/backend/core/reasoning_schemas.py b/backend/core/reasoning_schemas.py index 48535f684..e569a4b0c 100644 Binary files a/backend/core/reasoning_schemas.py and b/backend/core/reasoning_schemas.py differ diff --git a/backend/main.py b/backend/main.py index f6e325752..8535371ec 100644 --- a/backend/main.py +++ b/backend/main.py @@ -3,6 +3,8 @@ """ from fastapi import FastAPI, Depends, HTTPException, BackgroundTasks, Request from fastapi.middleware.cors import CORSMiddleware +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse from sqlalchemy.orm import Session from pydantic import BaseModel from typing import List, Optional, Dict, Any @@ -10,6 +12,8 @@ import uvicorn import logging import hashlib +import json +import time from dotenv import load_dotenv # 加载环境变量 @@ -47,6 +51,64 @@ description="大疆无人机智能销售助理系统", version="0.1.0" ) +# region agent log +def _debug_log(hypothesis_id: str, location: str, message: str, data: Dict[str, Any]) -> None: + try: + with open("/Users/timothylin/Globot/.cursor/debug.log", "a", encoding="utf-8") as f: + f.write(json.dumps({ + "runId": "pre-fix", + "hypothesisId": hypothesis_id, + "location": location, + "message": message, + "data": data, + "timestamp": int(time.time() * 1000), + }, ensure_ascii=True) + "\n") + except Exception: + pass +# endregion + + +@app.middleware("http") +async def maritime_upload_debug_middleware(request: Request, call_next): + if request.url.path == "/api/v2/maritime/documents/upload": + # region agent log + _debug_log( + "H4", + "backend/main.py:maritime_upload_debug_middleware:entry", + "upload request received at middleware", + { + "method": request.method, + "contentType": request.headers.get("content-type"), + "contentLength": request.headers.get("content-length"), + }, + ) + # endregion + response = await call_next(request) + if request.url.path == "/api/v2/maritime/documents/upload": + # region agent log + _debug_log( + "H5", + "backend/main.py:maritime_upload_debug_middleware:exit", + "upload request completed in middleware", + {"statusCode": response.status_code}, + ) + # endregion + return response + + +@app.exception_handler(RequestValidationError) +async def request_validation_exception_debug_handler(request: Request, exc: RequestValidationError): + if request.url.path == "/api/v2/maritime/documents/upload": + # region agent log + _debug_log( + "H1", + "backend/main.py:request_validation_exception_debug_handler", + "upload request validation failed", + {"errors": exc.errors()}, + ) + # endregion + return JSONResponse(status_code=422, content={"detail": exc.errors()}) + # 注册路由 app.include_router(demo_router) app.include_router(market_sentinel_router) diff --git a/backend/requirements.txt b/backend/requirements.txt index 2ed09c512..7e9478987 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -39,3 +39,12 @@ requests==2.32.5 # Platform-specific uvloop==0.22.1; platform_system != "Windows" + +#langchain and transformers dependencies +transformers==4.31.0 +sentence-transformers==2.3.0 +langchain==0.0.208 +langchain-chroma==0.0.208 +chromadb==0.3.24 + + diff --git a/backend/services/maritime_knowledge_base.py b/backend/services/maritime_knowledge_base.py index e0476f1f7..67c146f00 100644 --- a/backend/services/maritime_knowledge_base.py +++ b/backend/services/maritime_knowledge_base.py @@ -14,6 +14,23 @@ logger = logging.getLogger(__name__) settings = get_settings() +DEBUG_LOG_PATH = "/Users/timothylin/Globot/.cursor/debug.log" + + +def _debug_log(run_id: str, hypothesis_id: str, location: str, message: str, data: Dict[str, Any]) -> None: + try: + payload = { + "runId": run_id, + "hypothesisId": hypothesis_id, + "location": location, + "message": message, + "data": data, + "timestamp": __import__("time").time_ns() // 1_000_000, + } + with open(DEBUG_LOG_PATH, "a", encoding="utf-8") as f: + f.write(json.dumps(payload, ensure_ascii=True) + "\n") + except Exception: + pass @dataclass @@ -52,12 +69,25 @@ def __init__(self): self.reranker = None self.bm25_indices: Dict[str, Any] = {} self.doc_maps: Dict[str, Dict[str, Document]] = {} - + # Initialize Gemini embeddings self.embeddings = GoogleGenerativeAIEmbeddings( - model="gemini-embedding-001", + model="models/gemini-embedding-001", google_api_key=settings.google_api_key ) + # region agent log + _debug_log( + "pre-fix", + "H2", + "maritime_knowledge_base.py:__init__", + "Embeddings object created", + { + "embeddings_class": type(self.embeddings).__name__, + "embeddings_model_attr": str(getattr(self.embeddings, "model", None)), + "has_client_attr": hasattr(self.embeddings, "client"), + }, + ) + # endregion # Create a Chroma collection for each defined collection for collection_name in self.COLLECTIONS.keys(): @@ -771,12 +801,65 @@ def add_user_document(self, doc_id: str, text: str, metadata: Dict[str, Any]) -> if not content: content = f"[Document: {metadata.get('title', 'Untitled')}] No text content extracted." logger.warning(f"Document {doc_id} has no text content, using placeholder") + # region agent log + _debug_log( + "pre-fix", + "H4", + "maritime_knowledge_base.py:add_user_document", + "Using placeholder content branch", + { + "doc_id": doc_id, + "title_present": bool(metadata.get("title")), + }, + ) + # endregion doc = Document(page_content=content, metadata=metadata) + # region agent log + _debug_log( + "pre-fix", + "H1", + "maritime_knowledge_base.py:add_user_document", + "Before add_documents", + { + "doc_id": doc_id, + "content_len": len(content), + "content_is_placeholder": content.startswith("[Document:"), + "embedding_model_attr": str(getattr(self.embeddings, "model", None)), + "collection_name": "user_documents", + }, + ) + # endregion collection.add_documents([doc], ids=[doc_id]) + # region agent log + _debug_log( + "pre-fix", + "H5", + "maritime_knowledge_base.py:add_user_document", + "add_documents succeeded", + { + "doc_id": doc_id, + "metadata_key_count": len(metadata.keys()), + }, + ) + # endregion logger.info(f"Added user document {doc_id} to ChromaDB") return doc_id except Exception as e: + # region agent log + _debug_log( + "pre-fix", + "H3", + "maritime_knowledge_base.py:add_user_document", + "add_documents failed", + { + "doc_id": doc_id, + "exception_type": type(e).__name__, + "exception_message": str(e), + "embedding_model_attr": str(getattr(self.embeddings, "model", None)), + }, + ) + # endregion logger.error(f"Error adding user document {doc_id}: {e}") raise diff --git a/backend/services/visual_risk_service.py b/backend/services/visual_risk_service.py index a55977f50..b254bcd3a 100644 --- a/backend/services/visual_risk_service.py +++ b/backend/services/visual_risk_service.py @@ -17,11 +17,13 @@ from datetime import datetime import json import os +import time from config import get_settings logger = logging.getLogger(__name__) settings = get_settings() +DEBUG_LOG_PATH = "/Users/timothylin/Globot/.cursor/debug.log" @dataclass @@ -191,6 +193,21 @@ def __init__(self): self.base_url = "https://generativelanguage.googleapis.com/v1beta" self._client: Optional[httpx.AsyncClient] = None logger.info(f"VisualRiskAnalyzer initialized (Gemini: {bool(self.api_key)}, Maps: {bool(self.maps_api_key)})") + + def _agent_debug_log(self, run_id: str, hypothesis_id: str, location: str, message: str, data: Dict[str, Any]) -> None: + try: + payload = { + "runId": run_id, + "hypothesisId": hypothesis_id, + "location": location, + "message": message, + "data": data, + "timestamp": int(time.time() * 1000), + } + with open(DEBUG_LOG_PATH, "a", encoding="utf-8") as f: + f.write(json.dumps(payload, ensure_ascii=True) + "\n") + except Exception: + pass async def _get_client(self) -> httpx.AsyncClient: if self._client is None or self._client.is_closed: @@ -203,6 +220,21 @@ async def close(self): async def download_satellite_image(self, lat: float, lon: float, zoom: int = 14) -> Optional[bytes]: """Download satellite image from Google Static Maps API""" + # region agent log + self._agent_debug_log( + "pre-fix", + "H1", + "services/visual_risk_service.py:download_satellite_image:entry", + "download_satellite_image called", + { + "lat": lat, + "lon": lon, + "zoom": zoom, + "has_maps_api_key": bool(self.maps_api_key), + "maps_key_equals_gemini_key": bool(self.maps_api_key and self.api_key and self.maps_api_key == self.api_key), + }, + ) + # endregion agent log if not self.maps_api_key: logger.warning("No Maps API key for satellite image download") return None @@ -219,6 +251,15 @@ async def download_satellite_image(self, lat: float, lon: float, zoom: int = 14) try: client = await self._get_client() response = await client.get(url, params=params) + # region agent log + self._agent_debug_log( + "pre-fix", + "H2", + "services/visual_risk_service.py:download_satellite_image:after_get", + "google static maps response", + {"status_code": response.status_code, "content_type": response.headers.get("content-type", "")}, + ) + # endregion agent log if response.status_code == 200: logger.info(f"Fetched satellite image from Google Maps: 200 OK") return response.content @@ -248,6 +289,22 @@ async def analyze_image( Returns: VisualRiskResult with detected risks """ + # region agent log + self._agent_debug_log( + "pre-fix", + "H3", + "services/visual_risk_service.py:analyze_image:entry", + "analyze_image called", + { + "has_image_path": bool(image_path), + "has_image_bytes": bool(image_bytes), + "mime_type": mime_type, + "has_coordinates": bool(coordinates), + "has_gemini_key": bool(self.api_key), + "has_maps_key": bool(self.maps_api_key), + }, + ) + # endregion agent log # Load image bytes from coordinates if provided and valid if coordinates and not image_bytes and not image_path: logger.info(f"Fetching satellite image for coordinates: {coordinates}") @@ -271,6 +328,15 @@ async def analyze_image( if not image_bytes: logger.warning("No image provided (and fetch failed), returning demo result") + # region agent log + self._agent_debug_log( + "pre-fix", + "H4", + "services/visual_risk_service.py:analyze_image:no_image_branch", + "falling back because no image bytes", + {"had_coordinates": bool(coordinates), "had_image_path": bool(image_path)}, + ) + # endregion agent log return self._get_demo_result() # Check if API is configured @@ -281,6 +347,15 @@ async def analyze_image( # Call Gemini Vision API try: result = await self._call_gemini_vision(image_bytes, mime_type) + # region agent log + self._agent_debug_log( + "pre-fix", + "H5", + "services/visual_risk_service.py:analyze_image:exit_success", + "analyze_image returning vision result", + {"risk_type": result.risk_type, "source_type": result.source_type}, + ) + # endregion agent log return result except Exception as e: logger.error(f"Gemini Vision API error: {e}") diff --git a/docs/OPENAI_API_PLAN.md b/docs/OPENAI_API_PLAN.md new file mode 100644 index 000000000..3df09066a --- /dev/null +++ b/docs/OPENAI_API_PLAN.md @@ -0,0 +1,58 @@ +# OpenAI / Codex maintenance plan + +## Purpose + +Globot is a maintained open-source decision-support prototype for supply-chain disruption review. If granted Codex for Open Source API credits (and ChatGPT Pro with Codex), the primary maintainer will use them for **real repository maintenance** first, and for **opt-in product experiments** second. + +This plan is intentionally concrete: credits should reduce review load and raise code quality without removing human accountability. + +## How API credits will be used + +### 1. Pull-request review automation (primary) + +- Summarise diffs for large frontend map / backend agent changes +- Flag missing tests, secret leaks, and HITL-control regressions +- Draft structured review checklists; a human maintainer must approve or request changes + +### 2. Issue triage and contributor onboarding + +- Classify bugs vs questions vs features +- Detect incomplete reproduction steps and draft respectful follow-ups for maintainer edit +- Propose “good first issue” labels for safer entry points into the codebase + +### 3. Release and documentation workflows + +- Draft changelog entries and migration notes from merged PRs +- Keep README / ROADMAP / SECURITY wording aligned with behaviour changes +- Generate dependency-update risk notes when Dependabot opens PRs + +### 4. Opt-in product assistance (secondary, server-side only) + +- Environment-configured assistance for summarising **fixture documents** and explaining scenario evidence in the demo path +- No OpenAI key in the client bundle; no model output may execute operational, compliance, or financial actions +- All such UI content labelled as model-assisted and reviewable + +### 5. Security-oriented review (Codex Security interest) + +- Prioritise auth surfaces, CORS, env handling, upload paths, and dependency CVEs when access is granted +- Coordinate fixes privately per [SECURITY.md](../SECURITY.md) when needed + +## Guardrails and evaluation + +| Guardrail | Rule | +| --- | --- | +| Credentials | Server-side only; never committed | +| Data | Representative, non-sensitive fixtures for development and evaluation | +| Accountability | Humans remain responsible for merges, security disclosure, and consequential recommendations | +| Labelling | Model-assisted content is labelled; sources/provenance retained where available | +| Metrics | Track triage accuracy, review usefulness, latency, and maintainer acceptance before expanding scope | + +## Success criteria + +Credits are successful if, within two release cycles, Globot shows: + +1. Faster, more consistent PR review notes with no increase in merge regressions +2. Clearer issue responses and fewer stalled “needs info” tickets +3. Documented evaluation results for any opt-in product experiment, including known failure modes + +Integration configuration, evaluation results, limitations, and breaking changes will be recorded in public pull requests and release notes. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 000000000..18ec934d5 --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,31 @@ +# Roadmap + +This roadmap describes public priorities for Globot. Items may be reordered as contributors join and as evaluation results arrive. Status is tracked through GitHub issues and pull requests. + +## Near term (next 1–2 months) + +- [ ] Expand CI beyond syntax/build: FastAPI route smoke tests and key scenario calculation fixtures +- [ ] Document every mock data boundary in the UI and API responses +- [ ] Add an explicit “provenance” panel for sources that back a recommendation summary +- [ ] Harden contribution path: first-time contributor labels, clearer issue templates, dependency update cadence via Dependabot +- [ ] Optional OpenAI-assisted **maintainer** workflows (issue triage drafts, PR review checklists) with human approval before any reply is posted — see [OPENAI_API_PLAN.md](OPENAI_API_PLAN.md) + +## Mid term (3–6 months) + +- [ ] Pluggable adapters for news / AIS / document corpora behind the same review contracts +- [ ] Evaluation harness for model-assisted summaries (faithfulness, latency, maintainer acceptance) +- [ ] Accessibility pass on high-traffic demo screens +- [ ] Versioned releases with changelog-driven notes and tagged GitHub Releases +- [ ] Codex Security–style review for dependency and auth surfaces when appropriate + +## Longer term + +- [ ] Shared OSS reference patterns for HITL logistics decision support +- [ ] Community scenarios contributed under clearly licensed sample data +- [ ] Optional multi-maintainer review rotation as contributor load grows + +## Non-goals + +- Autonomous execution against live trading, booking, or payment systems +- Shipping production credentials or personal allowlists in the repository +- Claiming mock demos as live sanctions / market truth diff --git a/frontend/.env.example b/frontend/.env.example index fd61ea009..06b89e980 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,2 +1,3 @@ # Clerk Configuration VITE_CLERK_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx +VITE_API_BASE_URL=http://localhost:8000 diff --git a/frontend/package.json b/frontend/package.json index 805fc23fc..c46074c8f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "dji-sales-frontend", + "name": "globot-frontend", "private": true, "version": "1.0.0", "type": "module", @@ -105,10 +105,5 @@ "react-dom": { "optional": true } - }, - "pnpm": { - "overrides": { - "vite": "6.3.5" - } } -} \ No newline at end of file +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 6f16ead75..b7487e5ec 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -1848,66 +1848,79 @@ packages: resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.55.1': resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.55.1': resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.55.1': resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.55.1': resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.55.1': resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.55.1': resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.55.1': resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.55.1': resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.55.1': resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.55.1': resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.55.1': resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.55.1': resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.55.1': resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} @@ -1977,24 +1990,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.12': resolution: {integrity: sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.12': resolution: {integrity: sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.12': resolution: {integrity: sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.12': resolution: {integrity: sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==} @@ -3162,24 +3179,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.1: resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.1: resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.1: resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.1: resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} @@ -3470,8 +3491,8 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} point-in-polygon-hao@1.2.4: @@ -4182,8 +4203,8 @@ packages: tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} tinyqueue@3.0.0: @@ -7286,9 +7307,9 @@ snapshots: dependencies: strnum: 1.1.2 - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.5 fecha@4.2.3: {} @@ -8009,7 +8030,7 @@ snapshots: picocolors@1.1.1: {} - picomatch@4.0.3: {} + picomatch@4.0.5: {} point-in-polygon-hao@1.2.4: dependencies: @@ -8879,10 +8900,10 @@ snapshots: tinycolor2@1.6.0: {} - tinyglobby@0.2.15: + tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 tinyqueue@3.0.0: {} @@ -9005,11 +9026,11 @@ snapshots: vite@6.3.5(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.0): dependencies: esbuild: 0.25.12 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 postcss: 8.5.6 rollup: 4.55.1 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 25.0.9 fsevents: 2.3.3 diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml new file mode 100644 index 000000000..442d6b46d --- /dev/null +++ b/frontend/pnpm-workspace.yaml @@ -0,0 +1,8 @@ +# pnpm 11+ settings (package.json "pnpm" field is ignored in v11) +overrides: + vite: 6.3.5 + +allowBuilds: + esbuild: true + '@clerk/shared': true + '@tailwindcss/oxide': true diff --git a/frontend/src/components/AgentCoTPanel.tsx b/frontend/src/components/AgentCoTPanel.tsx index 5fabef772..3f99bbeef 100644 --- a/frontend/src/components/AgentCoTPanel.tsx +++ b/frontend/src/components/AgentCoTPanel.tsx @@ -129,6 +129,7 @@ interface AgentCoTPanelProps { executionSummary?: ExecutionSummary | null; awaitingConfirmation?: boolean; onConfirmDecision?: (action: string) => void; + selectedRoute?: { name: string; distance: number; estimatedTime: number; riskLevel: string; waypointNames: string[]; description: string } | null; } type TabType = "stream" | "debate" | "decision" | "execution"; @@ -148,6 +149,7 @@ export function AgentCoTPanel({ executionSummary = null, awaitingConfirmation = false, onConfirmDecision, + selectedRoute = null, }: AgentCoTPanelProps) { const [activeTab, setActiveTab] = useState("stream"); const [isCollapsed, setIsCollapsed] = useState(false); @@ -291,6 +293,22 @@ export function AgentCoTPanel({ ))} + {/* Active Route Context */} + {selectedRoute && ( +
+
+
+
+ {selectedRoute.name} +
+
+ {selectedRoute.distance.toLocaleString()} nm + ~{selectedRoute.estimatedTime}d +
+
+
+ )} + {/* Content */}
void; + selectedRoute?: { name: string; distance: number; estimatedTime: number; riskLevel: string; waypointNames: string[]; description: string } | null; + // Hedge data + hedgeRiskData?: RiskAssessment | null; + hedgeRecommendation?: HedgeRecommendation | null; + hedgeLoading?: boolean; + hedgeError?: string | null; + onRunHedge?: () => void; + // CoT / Debate / Execution + isCotActive?: boolean; + debateCount?: number; + executionPhase?: 'pending' | 'executing' | 'complete'; } export const AgentWorkflow: React.FC = ({ @@ -53,8 +65,18 @@ export const AgentWorkflow: React.FC = ({ marketSentinelLoading, marketSentinelError, onRunMarketSentinel, + selectedRoute, + hedgeRiskData, + hedgeRecommendation, + hedgeLoading, + hedgeError, + onRunHedge, + isCotActive, + debateCount = 0, + executionPhase = 'pending', }) => { const [agents, setAgents] = useState(INITIAL_AGENTS); + const [selectedAgentId, setSelectedAgentId] = useState<"market_sentinel" | "risk_hedger" | null>(null); // Helper to get Market Sentinel status based on real API data const getMarketSentinelStatus = (): { status: AgentStatus; lastAction: string } => { @@ -91,13 +113,128 @@ export const AgentWorkflow: React.FC = ({ }; }; + // Helper to get Risk Hedger status from real hedge API data + const getHedgeStatus = (): { status: AgentStatus; lastAction: string } => { + if (hedgeLoading) { + return { + status: "thinking", + lastAction: "Analyzing financial exposure and hedging options...", + }; + } + + if (hedgeError) { + return { + status: "alert", + lastAction: `Error: ${hedgeError}`, + }; + } + + if (hedgeRecommendation) { + return { + status: "completed", + lastAction: `Strategy ready: ${hedgeRecommendation.regime} regime · Fuel hedge ${hedgeRecommendation.fuel_hedging?.hedge_ratio || 'N/A'}`, + }; + } + + if (hedgeRiskData) { + const isUrgent = hedgeRiskData.urgency === 'CRITICAL' || hedgeRiskData.urgency === 'HIGH'; + return { + status: isUrgent ? "alert" : "completed", + lastAction: `${hedgeRiskData.urgency}: $${(hedgeRiskData.total_var_95_usd / 1000).toFixed(0)}K VaR · ${hedgeRiskData.market_regime} regime`, + }; + } + + return { + status: "idle", + lastAction: "Standing by for financial exposure analysis", + }; + }; + + // Helper to get Logistics Orchestrator status from route + execution phase + const getLogisticsStatus = (): { status: AgentStatus; lastAction: string } => { + if (executionPhase === 'executing') { + return { + status: "thinking", + lastAction: "Executing route optimization and carrier allocation...", + }; + } + + if (executionPhase === 'complete') { + return { + status: "completed", + lastAction: selectedRoute + ? `Route optimized: ${selectedRoute.name} (${selectedRoute.distance.toLocaleString()} nm)` + : "Route execution complete", + }; + } + + if (selectedRoute) { + return { + status: "completed", + lastAction: `Active route: ${selectedRoute.name} · ~${selectedRoute.estimatedTime}d`, + }; + } + + return { + status: "idle", + lastAction: "Ready to optimize shipping routes", + }; + }; + + // Helper to get Adversarial Debate status from CoT/debate events + const getDebateStatus = (): { status: AgentStatus; lastAction: string } => { + if (debateCount > 0 && isCotActive) { + return { + status: "thinking", + lastAction: `Reviewing ${debateCount} debate exchange${debateCount > 1 ? 's' : ''} — challenging assumptions...`, + }; + } + + if (debateCount > 0 && !isCotActive) { + return { + status: "completed", + lastAction: `Completed ${debateCount} adversarial review${debateCount > 1 ? 's' : ''}`, + }; + } + + if (isCotActive) { + return { + status: "thinking", + lastAction: "CoT active — preparing adversarial challenges...", + }; + } + + // Fallback: derive from Market Sentinel confidence + if (marketSentinelData?.signal_packet) { + const confidence = marketSentinelData.signal_packet.confidence; + if (confidence < 0.8) { + return { + status: "thinking", + lastAction: `Reviewing signal confidence: ${(confidence * 100).toFixed(0)}%`, + }; + } + return { + status: "completed", + lastAction: "Signal validated with high confidence", + }; + } + + return { + status: "idle", + lastAction: "Ready for adversarial review", + }; + }; + useEffect(() => { if (!isLive) return; const t = currentTime % 60; + const isSentinelSelected = selectedAgentId === "market_sentinel"; + const isHedgeSelected = selectedAgentId === "risk_hedger"; - // Use real Market Sentinel data if available, otherwise fallback to simulation - const sentinelState = marketSentinelData || marketSentinelLoading + const sentinelState = isSentinelSelected + ? ( + marketSentinelData || marketSentinelLoading ? getMarketSentinelStatus() : { status: (t > 5 && t < 15 ? "thinking" : t >= 15 ? "alert" : "idle") as AgentStatus, @@ -106,80 +243,100 @@ export const AgentWorkflow: React.FC = ({ : t > 5 ? "Scanning Reuters, Bloomberg for supply chain disruptions..." : "Monitoring global news feeds for supply chain disruptions", + } + ) + : { + status: "idle" as AgentStatus, + lastAction: "Select Market Sentinel to run", }; - // Derive other agent states based on Market Sentinel results + // Derive other agent states — prefer real data, fallback to timer simulation const hasRealSignal = marketSentinelData?.signal_packet; - const signalSeverity = hasRealSignal ? marketSentinelData.signal_packet.severity : null; - const isHighRisk = signalSeverity === 'CRITICAL' || signalSeverity === 'HIGH'; + const hasRealHedge = hedgeRiskData || hedgeRecommendation || hedgeLoading || hedgeError; - setAgents([ - { - id: "market_sentinel", - ...sentinelState, - }, - { - id: "risk_hedger", - status: hasRealSignal - ? (isHighRisk ? "alert" : "completed") - : (t > 15 && t < 25 ? "alert" : t >= 25 && t < 35 ? "thinking" : t >= 35 ? "completed" : "idle"), - lastAction: hasRealSignal - ? (isHighRisk - ? `CRITICAL: ${marketSentinelData.signal_packet.affected_lanes.length} lanes affected` - : "Financial exposure analysis complete") - : (t >= 35 - ? "Recalculated portfolio exposure across alternative routes" - : t >= 25 - ? "Analyzing financial exposure and hedging options..." - : t > 15 - ? "CRITICAL: Elevated risk detected in primary corridor" - : "Standing by for financial exposure analysis"), - }, - { - id: "logistics", - status: hasRealSignal - ? (isHighRisk ? "thinking" : "completed") - : (t > 28 && t < 40 ? "thinking" : t >= 40 ? "completed" : "idle"), - lastAction: hasRealSignal - ? (isHighRisk - ? "Calculating alternative routes for affected lanes..." - : "Route optimization complete") - : (t >= 40 - ? "Secured alternative carrier capacity for 12 high-priority shipments" - : t > 28 - ? "Negotiating with port authorities and carriers..." - : "Ready to optimize shipping routes"), - }, - { - id: "compliance", - status: hasRealSignal - ? "completed" - : (t > 30 && t < 38 ? "thinking" : t >= 38 ? "completed" : "idle"), - lastAction: hasRealSignal - ? `Validated against ${marketSentinelData.signal_packet.entities.length} monitored entities` - : (t >= 38 - ? "Validated alternative routes against 89 international regulations" - : t > 30 - ? "Checking OFAC, UN sanctions lists for route compliance..." - : "Awaiting regulatory validation requests"), - }, - { - id: "debate", - status: hasRealSignal - ? (marketSentinelData.signal_packet.confidence < 0.8 ? "thinking" : "completed") - : (t > 40 && t < 50 ? "thinking" : t >= 50 ? "completed" : "idle"), - lastAction: hasRealSignal - ? (marketSentinelData.signal_packet.confidence < 0.8 - ? `Reviewing signal confidence: ${(marketSentinelData.signal_packet.confidence * 100).toFixed(0)}%` - : "Signal validated with high confidence") - : (t >= 50 + const hedgeState = isHedgeSelected + ? ( + hasRealHedge + ? getHedgeStatus() + : { + status: (hasRealSignal + ? ((hasRealSignal && (marketSentinelData!.signal_packet.severity === 'CRITICAL' || marketSentinelData!.signal_packet.severity === 'HIGH')) ? "alert" : "completed") + : (t > 15 && t < 25 ? "alert" : t >= 25 && t < 35 ? "thinking" : t >= 35 ? "completed" : "idle") + ) as AgentStatus, + lastAction: hasRealSignal + ? ((marketSentinelData!.signal_packet.severity === 'CRITICAL' || marketSentinelData!.signal_packet.severity === 'HIGH') + ? `CRITICAL: ${marketSentinelData!.signal_packet.affected_lanes.length} lanes affected` + : "Financial exposure analysis complete") + : (t >= 35 + ? "Recalculated portfolio exposure across alternative routes" + : t >= 25 + ? "Analyzing financial exposure and hedging options..." + : t > 15 + ? "CRITICAL: Elevated risk detected in primary corridor" + : "Standing by for financial exposure analysis"), + } + ) + : { + status: "idle" as AgentStatus, + lastAction: "Select Risk Hedger to run", + }; + + // Logistics: real data if execution phase or route is set + const hasRealLogistics = executionPhase !== 'pending' || selectedRoute; + const logisticsState = hasRealLogistics + ? getLogisticsStatus() + : { + status: (t > 28 && t < 40 ? "thinking" : t >= 40 ? "completed" : "idle") as AgentStatus, + lastAction: t >= 40 + ? "Secured alternative carrier capacity for 12 high-priority shipments" + : t > 28 + ? "Negotiating with port authorities and carriers..." + : "Ready to optimize shipping routes", + }; + + // Compliance: if we have a real signal, use entity count; else timer + const complianceState = hasRealSignal + ? { + status: "completed" as AgentStatus, + lastAction: `Validated against ${marketSentinelData!.signal_packet.entities.length} monitored entities`, + } + : { + status: (t > 30 && t < 38 ? "thinking" : t >= 38 ? "completed" : "idle") as AgentStatus, + lastAction: t >= 38 + ? "Validated alternative routes against 89 international regulations" + : t > 30 + ? "Checking OFAC, UN sanctions lists for route compliance..." + : "Awaiting regulatory validation requests", + }; + + // Debate: real data if CoT or debates exist + const hasRealDebate = isCotActive || debateCount > 0; + const debateState = hasRealDebate + ? getDebateStatus() + : hasRealSignal + ? { + status: (marketSentinelData!.signal_packet.confidence < 0.8 ? "thinking" : "completed") as AgentStatus, + lastAction: marketSentinelData!.signal_packet.confidence < 0.8 + ? `Reviewing signal confidence: ${(marketSentinelData!.signal_packet.confidence * 100).toFixed(0)}%` + : "Signal validated with high confidence", + } + : { + status: (t > 40 && t < 50 ? "thinking" : t >= 50 ? "completed" : "idle") as AgentStatus, + lastAction: t >= 50 ? "Southern route cost estimates appear optimistic" : t > 40 ? "Challenging decision logic and assumptions..." - : "Ready for adversarial review"), - }, + : "Ready for adversarial review", + }; + + setAgents([ + { id: "market_sentinel", ...sentinelState }, + { id: "risk_hedger", ...hedgeState }, + { id: "logistics", ...logisticsState }, + { id: "compliance", ...complianceState }, + { id: "debate", ...debateState }, ]); - }, [currentTime, isLive, marketSentinelData, marketSentinelLoading, marketSentinelError]); + }, [currentTime, isLive, marketSentinelData, marketSentinelLoading, marketSentinelError, hedgeRiskData, hedgeRecommendation, hedgeLoading, hedgeError, isCotActive, debateCount, executionPhase, selectedRoute, selectedAgentId]); const getAgentById = (id: string) => agents.find(a => a.id === id); @@ -204,20 +361,60 @@ export const AgentWorkflow: React.FC = ({
- {onRunMarketSentinel && ( - - )} +
+ {onRunMarketSentinel && ( + + )} + {onRunHedge && ( + + )} +
+ {/* Active Route Context */} + {selectedRoute && ( +
+
+
+ Active Route +
+

{selectedRoute.name}

+
+ {selectedRoute.distance.toLocaleString()} nm + ~{selectedRoute.estimatedTime}d + + {selectedRoute.riskLevel} risk + +
+
+ )} + {/* Signal Alert Banner */} - {marketSentinelData?.signal_packet && ( + {selectedAgentId === "market_sentinel" && marketSentinelData?.signal_packet && (
= ({
)} + {/* Hedge Data Banner */} + {selectedAgentId === "risk_hedger" && hedgeRiskData && ( +
+
+ + {hedgeRiskData.urgency} · {hedgeRiskData.market_regime} + + + Risk Hedge + +
+
+ Exposure: ${(hedgeRiskData.total_exposure_usd / 1_000_000).toFixed(1)}M + + VaR 95%: ${(hedgeRiskData.total_var_95_usd / 1_000).toFixed(0)}K +
+ {hedgeRecommendation && ( +
+

+ Strategy: {hedgeRecommendation.regime} regime + {hedgeRecommendation.fuel_hedging && ( + <> · Fuel hedge: {hedgeRecommendation.fuel_hedging.hedge_ratio} + )} +

+
+ )} +
+ )} +
+ + +
); diff --git a/frontend/src/components/CompliancePanel.tsx b/frontend/src/components/CompliancePanel.tsx new file mode 100644 index 000000000..2d6c9cd9b --- /dev/null +++ b/frontend/src/components/CompliancePanel.tsx @@ -0,0 +1,749 @@ +/** + * CompliancePanel - Sidebar-optimized compliance analysis panel + * + * Provides full compliance workflow inside the DemoPage sidebar: + * - Clerk user provisioning + * - Port selector with search (reuses MAJOR_PORTS) + * - Saved route selection + * - Document count summary + * - Gap analysis via documentAPI.detectMissingDocuments() + * - GapAnalysisReport display + * + * Pre-populates with the DemoPage's current origin/destination ports. + */ + +import React, { useState, useEffect, useMemo, useCallback, useRef } from "react"; +import { useUser } from "@clerk/clerk-react"; +import { motion, AnimatePresence } from "motion/react"; +import { + Shield, + Search, + Navigation, + Anchor, + FileText, + ChevronRight, + Plus, + X, + AlertCircle, + RefreshCw, + Upload, + CheckCircle2, +} from "lucide-react"; +import { documentAPI } from "../services/documentApi"; +import type { VesselRoute, MissingDocsResponse, DocumentInfo } from "../services/documentApi"; +import { GapAnalysisReport } from "./documents"; +import { MAJOR_PORTS } from "../data/ports"; +import type { GlobalPort } from "../utils/routeCalculator"; + +// ---------- helpers ---------- + +const COUNTRY_CODE_MAP: Record = { + China: "CN", Singapore: "SG", Netherlands: "NL", Germany: "DE", + USA: "US", UK: "GB", Belgium: "BE", Spain: "ES", France: "FR", + Italy: "IT", Japan: "JP", "South Korea": "KR", India: "IN", + UAE: "AE", "Saudi Arabia": "SA", Malaysia: "MY", Thailand: "TH", + Vietnam: "VN", Indonesia: "ID", Philippines: "PH", Taiwan: "TW", + Australia: "AU", "New Zealand": "NZ", Brazil: "BR", Mexico: "MX", + Canada: "CA", Argentina: "AR", Chile: "CL", Colombia: "CO", + Peru: "PE", Panama: "PA", Egypt: "EG", Turkey: "TR", + "South Africa": "ZA", Kenya: "KE", Morocco: "MA", Nigeria: "NG", + Ghana: "GH", "Ivory Coast": "CI", Senegal: "SN", Tanzania: "TZ", + Djibouti: "DJ", Oman: "OM", Kuwait: "KW", Israel: "IL", + Greece: "GR", Poland: "PL", Sweden: "SE", Denmark: "DK", + Finland: "FI", Norway: "NO", Estonia: "EE", Latvia: "LV", + Russia: "RU", Ukraine: "UA", Romania: "RO", Ireland: "IE", + Portugal: "PT", Bangladesh: "BD", Pakistan: "PK", "Sri Lanka": "LK", + Malta: "MT", Jamaica: "JM", Bahamas: "BS", "Puerto Rico": "PR", + Uruguay: "UY", Ecuador: "EC", Iran: "IR", Mauritius: "MU", +}; + +interface PortEntry { + id: number; + name: string; + country: string; + region: string; + un_locode: string; + latitude: number; + longitude: number; +} + +function buildPortEntries(): PortEntry[] { + return MAJOR_PORTS.map((port, idx) => { + const cc = COUNTRY_CODE_MAP[port.country] || port.country.substring(0, 2).toUpperCase(); + const pc = port.name.replace(/\s+/g, "").substring(0, 3).toUpperCase(); + return { + id: idx + 1, + name: port.name, + country: port.country, + region: port.region, + un_locode: `${cc}${pc}`, + latitude: port.coordinates[1], + longitude: port.coordinates[0], + }; + }); +} + +// ---------- component ---------- + +export interface CompliancePanelProps { + originPort?: GlobalPort | null; + destinationPort?: GlobalPort | null; + activeMapRoute?: { name: string; distance: number; estimatedTime: number; riskLevel: string; waypointNames: string[]; description: string } | null; +} + +export function CompliancePanel({ originPort, destinationPort, activeMapRoute }: CompliancePanelProps) { + const { user } = useUser(); + + // identity + const [customerId, setCustomerId] = useState(null); + const [vesselId, setVesselId] = useState(null); + + // routes & docs + const [vesselRoutes, setVesselRoutes] = useState([]); + const [selectedRoute, setSelectedRoute] = useState(null); + const [vesselDocuments, setVesselDocuments] = useState([]); + + // port selector + const [selectedRoutePorts, setSelectedRoutePorts] = useState([]); + const [portSearchQuery, setPortSearchQuery] = useState(""); + const [showPortDropdown, setShowPortDropdown] = useState(false); + + // route creation + const [newRouteName, setNewRouteName] = useState(""); + const [isCreatingRoute, setIsCreatingRoute] = useState(false); + + // analysis + const [isAnalyzing, setIsAnalyzing] = useState(false); + const [analysisError, setAnalysisError] = useState(null); + const [missingDocsResult, setMissingDocsResult] = useState(null); + + // loading state + const [isLoading, setIsLoading] = useState(false); + + // upload state + const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); + const [uploadFile, setUploadFile] = useState(null); + const [uploadDescription, setUploadDescription] = useState(""); + const [isUploading, setIsUploading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); + const fileInputRef = useRef(null); + + const allPorts = useMemo(buildPortEntries, []); + + // ---- provision user ---- + useEffect(() => { + if (!user) return; + const provision = async () => { + try { + const res = await documentAPI.provisionUser({ + clerk_id: user.id, + email: user.primaryEmailAddress?.emailAddress || "", + name: user.fullName || undefined, + }); + setCustomerId(res.customer_id); + if (res.vessel_id) setVesselId(res.vessel_id); + } catch { + // fallback + setCustomerId(1); + setVesselId(1); + } + }; + provision(); + }, [user]); + + // ---- pre-populate ports from DemoPage origin/destination ---- + useEffect(() => { + if (selectedRoutePorts.length > 0) return; // user already picked ports + const initial: PortEntry[] = []; + if (originPort) { + const found = allPorts.find((p) => p.name === originPort.name); + if (found) initial.push(found); + } + if (destinationPort) { + const found = allPorts.find((p) => p.name === destinationPort.name); + if (found && !initial.find((p) => p.un_locode === found.un_locode)) initial.push(found); + } + if (initial.length > 0) setSelectedRoutePorts(initial); + }, [originPort, destinationPort]); // eslint-disable-line react-hooks/exhaustive-deps + + // ---- load compliance data once provisioned ---- + useEffect(() => { + if (!customerId) return; + const load = async () => { + setIsLoading(true); + try { + if (vesselId) { + const routes = await documentAPI.getVesselRoutes(vesselId); + setVesselRoutes(routes); + const active = routes.find((r) => r.is_active); + if (active) setSelectedRoute(active); + else if (routes.length > 0) setSelectedRoute(routes[0]); + } + const docs = await documentAPI.getCustomerDocuments(customerId); + setVesselDocuments(docs); + } catch { + // silent + } finally { + setIsLoading(false); + } + }; + load(); + }, [customerId, vesselId]); + + // ---- handlers ---- + + const handleCreateRoute = useCallback(async () => { + if (!newRouteName.trim() || selectedRoutePorts.length === 0 || !vesselId) return; + setIsCreatingRoute(true); + setAnalysisError(null); + try { + const portCodes = selectedRoutePorts.map((p) => p.un_locode); + const created = await documentAPI.createRoute(vesselId, { + route_name: newRouteName, + port_codes: portCodes, + set_active: true, + }); + setVesselRoutes((prev) => [created, ...prev]); + setSelectedRoute(created); + setNewRouteName(""); + } catch { + setAnalysisError("Failed to create route."); + } finally { + setIsCreatingRoute(false); + } + }, [newRouteName, selectedRoutePorts, vesselId]); + + const handleRunAnalysis = useCallback(async () => { + const hasRoute = selectedRoute && selectedRoute.port_codes?.length > 0; + const hasPorts = selectedRoutePorts.length > 0; + if (!customerId || (!hasRoute && !hasPorts)) { + setAnalysisError("Select a route or add ports first."); + return; + } + setIsAnalyzing(true); + setAnalysisError(null); + setMissingDocsResult(null); + try { + const portCodes = hasRoute ? selectedRoute!.port_codes : selectedRoutePorts.map((p) => p.un_locode); + const result = await documentAPI.detectMissingDocuments({ port_codes: portCodes, customer_id: customerId }); + setMissingDocsResult(result); + } catch (err: any) { + setAnalysisError(err?.response?.data?.detail || err?.message || "Analysis failed."); + } finally { + setIsAnalyzing(false); + } + }, [selectedRoute, selectedRoutePorts, customerId]); + + const refreshData = useCallback(async () => { + if (!customerId) return; + setIsLoading(true); + try { + const promises: Promise[] = [documentAPI.getCustomerDocuments(customerId)]; + if (vesselId) promises.unshift(documentAPI.getVesselRoutes(vesselId)); + const results = await Promise.all(promises); + if (vesselId) { + setVesselRoutes(results[0]); + setVesselDocuments(results[1]); + } else { + setVesselDocuments(results[0]); + } + } catch { + // silent + } finally { + setIsLoading(false); + } + }, [customerId, vesselId]); + + const handleFileSelect = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + setUploadFile(e.target.files[0]); + setUploadDescription(""); + // setIsUploadModalOpen(true); // Modal should already be open if triggered from button + e.target.value = ""; + } + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + const file = e.dataTransfer.files?.[0]; + if (file) { + setUploadFile(file); + setUploadDescription(""); + } + }; + + const handleUploadConfirm = async () => { + if (!uploadFile || !customerId || !vesselId) return; + setIsUploading(true); + setUploadProgress(0); + setAnalysisError(null); + + // Progress simulation + const interval = setInterval(() => { + setUploadProgress((prev) => { + if (prev >= 90) { + clearInterval(interval); + return 90; + } + return prev + 10; + }); + }, 300); + + try { + await documentAPI.uploadDocument({ + customer_id: customerId, + vessel_id: vesselId, + document_type: "other", + title: uploadDescription.trim() || uploadFile.name, + file: uploadFile, + }); + + setUploadProgress(100); + clearInterval(interval); + + // Short delay before closing + await new Promise((resolve) => setTimeout(resolve, 800)); + + setIsUploadModalOpen(false); + setUploadFile(null); + setUploadDescription(""); + refreshData(); + } catch (err) { + console.error("Upload failed", err); + clearInterval(interval); + setAnalysisError("Failed to upload document."); + setIsUploading(false); // Stop uploading state on error so user can retry + setUploadProgress(0); + } finally { + // setIsUploading(false); // Only set false on error or close + } + }; + + // ---- derived ---- + + const validCount = vesselDocuments.filter((d) => { + if (!d.expiry_date) return true; + return new Date(d.expiry_date) > new Date(); + }).length; + + const expiringCount = vesselDocuments.filter((d) => { + if (!d.expiry_date) return false; + const days = Math.ceil((new Date(d.expiry_date).getTime() - Date.now()) / 86_400_000); + return days > 0 && days <= 30; + }).length; + + const expiredCount = vesselDocuments.filter((d) => { + if (!d.expiry_date) return false; + return new Date(d.expiry_date) <= new Date(); + }).length; + + // filtered ports for dropdown + const filteredPorts = useMemo(() => { + const q = portSearchQuery.toLowerCase(); + return allPorts + .filter((p) => { + if (!q) return true; + return p.name.toLowerCase().includes(q) || p.country.toLowerCase().includes(q) || p.un_locode.toLowerCase().includes(q); + }) + .filter((p) => !selectedRoutePorts.find((sp) => sp.un_locode === p.un_locode)) + .slice(0, 12); + }, [allPorts, portSearchQuery, selectedRoutePorts]); + + // ---- render ---- + + return ( +
+ {/* Header row */} +
+ Compliance +
+ + + +
+
+ + {/* ---- Active Route Context ---- */} + {activeMapRoute && ( +
+
+ + Active Route +
+

{activeMapRoute.name}

+
+ {activeMapRoute.distance.toLocaleString()} nm + ~{activeMapRoute.estimatedTime}d + + {activeMapRoute.riskLevel} + +
+ {activeMapRoute.waypointNames.length > 0 && ( +
+ {activeMapRoute.waypointNames.slice(0, 5).map((wp, idx) => ( + + {wp} + {idx < Math.min(activeMapRoute.waypointNames.length, 5) - 1 && } + + ))} + {activeMapRoute.waypointNames.length > 5 && ( + +{activeMapRoute.waypointNames.length - 5} more + )} +
+ )} +
+ )} + + {/* ---- Route Selection ---- */} + {vesselRoutes.length > 0 && ( +
+ + + + {selectedRoute && ( +
+ {selectedRoute.port_codes.map((pc, idx) => ( + + {pc} + {idx < selectedRoute.port_codes.length - 1 && } + + ))} +
+ )} +
+ )} + + {/* ---- Port Selector ---- */} +
+
+ + {selectedRoutePorts.length > 0 && ( + + )} +
+ +
+ { setPortSearchQuery(e.target.value); setShowPortDropdown(true); }} + onFocus={() => setShowPortDropdown(true)} + onBlur={() => setTimeout(() => setShowPortDropdown(false), 200)} + placeholder="Search ports..." + className="w-full bg-[#0a0e1a] border border-white/10 rounded-lg px-3 py-2 text-xs focus:border-blue-500/50 outline-none transition-all placeholder:text-white/20" + /> + {showPortDropdown && filteredPorts.length > 0 && ( +
+ {filteredPorts.map((port) => ( + + ))} +
+ )} +
+ + {/* Selected port chips */} + {selectedRoutePorts.length > 0 && ( +
+ {selectedRoutePorts.map((port, idx) => ( +
+
+ {idx + 1} + {port.name} + {port.un_locode} +
+ +
+ ))} +
+ )} +
+ + {/* ---- Save as Route ---- */} + {vesselId && selectedRoutePorts.length > 0 && ( +
+ setNewRouteName(e.target.value)} + placeholder="Route name (optional, to save)" + className="w-full bg-[#0a0e1a] border border-white/10 rounded-lg px-3 py-2 text-xs focus:border-blue-500/50 outline-none transition-all placeholder:text-white/20" + /> + {newRouteName.trim() && ( + + )} +
+ )} + + {/* ---- Document Summary ---- */} + {vesselDocuments.length > 0 && ( +
+ + + +
+ )} + + {/* ---- Run Analysis Button ---- */} +
+ {analysisError && ( +
+ + {analysisError} +
+ )} + +
+ + {/* ---- Analysis Results ---- */} + {missingDocsResult && ( +
+ +
+ )} + + {/* ---- Upload Modal ---- */} + + {isUploadModalOpen && ( + + + {/* Decorative Elements */} +
+ + + +
+
+ +
+

Upload Document

+

Upload vessel certificates or shipping docs for compliance analysis

+
+ + {!isUploading ? ( +
+ {!uploadFile ? ( +
fileInputRef.current?.click()} + onDrop={handleDrop} + onDragOver={(e) => e.preventDefault()} + className="border-2 border-dashed border-white/10 rounded-2xl p-8 text-center hover:border-blue-500/40 hover:bg-blue-500/5 transition-all cursor-pointer group" + > + +

Drag and Drop Files Here

+

Supports PDF, PNG, JPG (Max 50MB)

+
+ +
+
+ ) : ( +
+
+ +
+

{uploadFile.name}

+

{(uploadFile.size / 1024).toFixed(0)} KB

+
+ +
+ +
+ +