katospiegel commited on
Commit
07c2476
·
verified ·
1 Parent(s): 1fef60d

Deploy develop: FastAPI+React frontend, multi-stage Docker (ai_agent serve)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .devcontainer/Dockerfile +16 -16
  2. .dockerignore +12 -6
  3. .env.dist +6 -0
  4. .gitattributes +9 -0
  5. .github/workflows/deploy-to-hf.yml +53 -53
  6. .github/workflows/deploy_docs.yml +57 -57
  7. .github/workflows/docker-build-pr.yml +23 -23
  8. .gitignore +4 -0
  9. AGENTS.md +92 -92
  10. CHANGELOG.md +21 -0
  11. Dockerfile +41 -33
  12. LICENSE +201 -201
  13. README.md +562 -562
  14. config.yaml +64 -64
  15. data/query.rq +8 -0
  16. docker-compose.yml +25 -0
  17. docs/architecture/agent.md +619 -619
  18. docs/architecture/catalog.md +455 -455
  19. docs/architecture/overview.md +459 -459
  20. docs/architecture/retrieval.md +387 -387
  21. docs/development/contributing.md +315 -315
  22. docs/development/structure.md +362 -362
  23. docs/development/testing.md +410 -410
  24. docs/getting-started/configuration.md +165 -165
  25. docs/getting-started/installation.md +143 -143
  26. docs/getting-started/quickstart.md +190 -190
  27. docs/guide.md +277 -277
  28. docs/index.md +81 -81
  29. docs/reference/changelog.md +79 -79
  30. docs/reference/cli.md +197 -197
  31. docs/reference/environment.md +309 -309
  32. docs/user-guide/advanced-features.md +387 -387
  33. docs/user-guide/chat-interface.md +275 -275
  34. docs/user-guide/file-formats.md +282 -282
  35. docs/user-guide/recommendations.md +329 -329
  36. docs/user-guide/running-demos.md +372 -372
  37. mkdocs.yml +108 -108
  38. pyproject.toml +4 -0
  39. src/ai_agent/agent/agent.py +650 -611
  40. src/ai_agent/agent/tools/__init__.py +38 -38
  41. src/ai_agent/agent/tools/deepwiki_tool.py +103 -103
  42. src/ai_agent/agent/tools/mcp/__init__.py +53 -53
  43. src/ai_agent/agent/tools/mcp/base.py +88 -88
  44. src/ai_agent/agent/tools/mcp/lungs_segmentation_tool.py +442 -442
  45. src/ai_agent/agent/tools/mcp/registry.py +203 -203
  46. src/ai_agent/agent/tools/query_utils.py +128 -128
  47. src/ai_agent/agent/tools/repo_info_tool.py +43 -32
  48. src/ai_agent/agent/tools/search_alternative_tool.py +73 -73
  49. src/ai_agent/agent/tools/sparql_tool.py +178 -0
  50. src/ai_agent/api/deps.py +103 -0
.devcontainer/Dockerfile CHANGED
@@ -1,17 +1,17 @@
1
- FROM ghcr.io/astral-sh/uv:python3.12-bookworm
2
-
3
- # Install just and other system dependencies
4
- RUN apt-get update && apt-get install -y \
5
- sudo \
6
- curl \
7
- && curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin \
8
- && apt-get clean \
9
- && rm -rf /var/lib/apt/lists/*
10
-
11
- # Create non-root user with UID/GID typically used by VS Code (1000:1000)
12
- RUN useradd -ms /bin/bash -u 1000 vscode \
13
- && apt-get update && apt-get install -y sudo \
14
- && echo "vscode ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
15
-
16
- USER vscode
17
  WORKDIR /workspaces
 
1
+ FROM ghcr.io/astral-sh/uv:python3.12-bookworm
2
+
3
+ # Install just and other system dependencies
4
+ RUN apt-get update && apt-get install -y \
5
+ sudo \
6
+ curl \
7
+ && curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin \
8
+ && apt-get clean \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Create non-root user with UID/GID typically used by VS Code (1000:1000)
12
+ RUN useradd -ms /bin/bash -u 1000 vscode \
13
+ && apt-get update && apt-get install -y sudo \
14
+ && echo "vscode ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
15
+
16
+ USER vscode
17
  WORKDIR /workspaces
.dockerignore CHANGED
@@ -1,6 +1,12 @@
1
- .git
2
- __pycache__
3
- *.pyc
4
- .env
5
- .env.*
6
- tests
 
 
 
 
 
 
 
1
+ .git
2
+ __pycache__
3
+ *.pyc
4
+ .env
5
+ .env.*
6
+ tests
7
+
8
+ # Frontend artifacts: copied via the node stage in the multi-stage build,
9
+ # so the python stage never needs the host's node_modules / dist tree.
10
+ src/frontend/node_modules
11
+ src/frontend/dist
12
+ src/frontend/.vite
.env.dist CHANGED
@@ -26,6 +26,12 @@ LOG_PROMPTS=0 # write selector prompt snapshots
26
  # Path to config.yaml
27
  CONFIG_PATH=path/to/custom/config.yaml
28
 
 
 
 
 
 
 
29
  # GraphDB
30
  GRAPHDB_GRAPH=
31
  GRAPHDB_URL=
 
26
  # Path to config.yaml
27
  CONFIG_PATH=path/to/custom/config.yaml
28
 
29
+ # Shared password for the new FastAPI frontend (leave empty to disable auth).
30
+ APP_PASSWORD=
31
+
32
+ # Session TTL in seconds for the in-process session store (defaults to 6h).
33
+ SESSION_TTL_SECONDS=21600
34
+
35
  # GraphDB
36
  GRAPHDB_GRAPH=
37
  GRAPHDB_URL=
.gitattributes ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ *.png filter=lfs diff=lfs merge=lfs -text
2
+ *.tif filter=lfs diff=lfs merge=lfs -text
3
+ *.tiff filter=lfs diff=lfs merge=lfs -text
4
+ *.jpg filter=lfs diff=lfs merge=lfs -text
5
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
6
+ *.gif filter=lfs diff=lfs merge=lfs -text
7
+ *.ico filter=lfs diff=lfs merge=lfs -text
8
+ *.webp filter=lfs diff=lfs merge=lfs -text
9
+ *.bmp filter=lfs diff=lfs merge=lfs -text
.github/workflows/deploy-to-hf.yml CHANGED
@@ -1,54 +1,54 @@
1
- name: Deploy to Hugging Face Space
2
-
3
- on:
4
- push:
5
- branches:
6
- - main
7
- workflow_dispatch:
8
-
9
- jobs:
10
- deploy:
11
- runs-on: ubuntu-latest
12
-
13
- steps:
14
- - name: Checkout code
15
- uses: actions/checkout@v4
16
- with:
17
- fetch-depth: 1
18
- lfs: false
19
-
20
- - name: Prepare clean HF deploy branch without assets
21
- run: |
22
- git config user.email "ci@github.actions"
23
- git config user.name "github-actions[bot]"
24
-
25
- original_readme="$(cat README.md)"
26
-
27
- cat > README.md <<EOF
28
- ---
29
- title: AI Agent
30
- emoji: 🤖
31
- colorFrom: blue
32
- colorTo: green
33
- sdk: docker
34
- app_port: 7860
35
- pinned: false
36
- ---
37
-
38
- $original_readme
39
- EOF
40
-
41
- rm -rf assets
42
-
43
- git checkout --orphan hf-deploy
44
-
45
- git add -A
46
- git commit -m "Deploy to Hugging Face Space"
47
-
48
- - name: Push to HF Space
49
- env:
50
- HF_TOKEN: ${{ secrets.HF_TOKEN }}
51
- run: |
52
- git push --force \
53
- https://SDSC:${HF_TOKEN}@huggingface.co/spaces/SDSC/ai-agent \
54
  hf-deploy:main
 
1
+ name: Deploy to Hugging Face Space
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ deploy:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - name: Checkout code
15
+ uses: actions/checkout@v4
16
+ with:
17
+ fetch-depth: 1
18
+ lfs: false
19
+
20
+ - name: Prepare clean HF deploy branch without assets
21
+ run: |
22
+ git config user.email "ci@github.actions"
23
+ git config user.name "github-actions[bot]"
24
+
25
+ original_readme="$(cat README.md)"
26
+
27
+ cat > README.md <<EOF
28
+ ---
29
+ title: AI Agent
30
+ emoji: 🤖
31
+ colorFrom: blue
32
+ colorTo: green
33
+ sdk: docker
34
+ app_port: 7860
35
+ pinned: false
36
+ ---
37
+
38
+ $original_readme
39
+ EOF
40
+
41
+ rm -rf assets
42
+
43
+ git checkout --orphan hf-deploy
44
+
45
+ git add -A
46
+ git commit -m "Deploy to Hugging Face Space"
47
+
48
+ - name: Push to HF Space
49
+ env:
50
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
51
+ run: |
52
+ git push --force \
53
+ https://SDSC:${HF_TOKEN}@huggingface.co/spaces/SDSC/ai-agent \
54
  hf-deploy:main
.github/workflows/deploy_docs.yml CHANGED
@@ -1,58 +1,58 @@
1
- name: mkdocs-ci
2
-
3
- on:
4
- push:
5
- branches:
6
- - main
7
-
8
- permissions:
9
- contents: read
10
- pages: write
11
- id-token: write
12
-
13
- concurrency:
14
- group: "pages"
15
- cancel-in-progress: true
16
-
17
- jobs:
18
- build:
19
- runs-on: ubuntu-latest
20
- steps:
21
- - name: Checkout repository
22
- uses: actions/checkout@v4
23
- with:
24
- fetch-depth: 0
25
-
26
- - name: Set up Pages
27
- uses: actions/configure-pages@v5
28
-
29
- - name: Set up Python
30
- uses: actions/setup-python@v5
31
- with:
32
- python-version: '3.10'
33
- cache: 'pip'
34
-
35
- - name: Install dependencies
36
- run: |
37
- pip install --upgrade pip
38
- pip install mkdocs-material
39
-
40
- - name: Build with MkDocs
41
- run: mkdocs build --strict
42
-
43
- - name: Upload artifact
44
- uses: actions/upload-pages-artifact@v4
45
- with:
46
- path: ./site
47
-
48
- deploy:
49
- if: github.ref == 'refs/heads/main'
50
- environment:
51
- name: github-pages
52
- url: ${{ steps.deployment.outputs.page_url }}
53
- runs-on: ubuntu-latest
54
- needs: build
55
- steps:
56
- - name: Deploy to GitHub Pages
57
- id: deployment
58
  uses: actions/deploy-pages@v4
 
1
+ name: mkdocs-ci
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: read
10
+ pages: write
11
+ id-token: write
12
+
13
+ concurrency:
14
+ group: "pages"
15
+ cancel-in-progress: true
16
+
17
+ jobs:
18
+ build:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - name: Checkout repository
22
+ uses: actions/checkout@v4
23
+ with:
24
+ fetch-depth: 0
25
+
26
+ - name: Set up Pages
27
+ uses: actions/configure-pages@v5
28
+
29
+ - name: Set up Python
30
+ uses: actions/setup-python@v5
31
+ with:
32
+ python-version: '3.10'
33
+ cache: 'pip'
34
+
35
+ - name: Install dependencies
36
+ run: |
37
+ pip install --upgrade pip
38
+ pip install mkdocs-material
39
+
40
+ - name: Build with MkDocs
41
+ run: mkdocs build --strict
42
+
43
+ - name: Upload artifact
44
+ uses: actions/upload-pages-artifact@v4
45
+ with:
46
+ path: ./site
47
+
48
+ deploy:
49
+ if: github.ref == 'refs/heads/main'
50
+ environment:
51
+ name: github-pages
52
+ url: ${{ steps.deployment.outputs.page_url }}
53
+ runs-on: ubuntu-latest
54
+ needs: build
55
+ steps:
56
+ - name: Deploy to GitHub Pages
57
+ id: deployment
58
  uses: actions/deploy-pages@v4
.github/workflows/docker-build-pr.yml CHANGED
@@ -1,24 +1,24 @@
1
- name: Docker build for PRs
2
-
3
- on:
4
- pull_request:
5
- branches: [ main ]
6
- workflow_dispatch:
7
-
8
- jobs:
9
- docker-build:
10
- runs-on: ubuntu-latest
11
-
12
- steps:
13
- - name: Checkout code
14
- uses: actions/checkout@v4
15
-
16
- - name: Set up Docker Buildx
17
- uses: docker/setup-buildx-action@v3
18
-
19
- - name: Build Docker image (no push)
20
- uses: docker/build-push-action@v6
21
- with:
22
- context: .
23
- push: false
24
  tags: ai-agent:pr-${{ github.event.number }}
 
1
+ name: Docker build for PRs
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [ main ]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ docker-build:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout code
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Set up Docker Buildx
17
+ uses: docker/setup-buildx-action@v3
18
+
19
+ - name: Build Docker image (no push)
20
+ uses: docker/build-push-action@v6
21
+ with:
22
+ context: .
23
+ push: false
24
  tags: ai-agent:pr-${{ github.event.number }}
.gitignore CHANGED
@@ -16,6 +16,10 @@ eggs/
16
  .eggs/
17
  lib/
18
  lib64/
 
 
 
 
19
  parts/
20
  sdist/
21
  var/
 
16
  .eggs/
17
  lib/
18
  lib64/
19
+ # Re-include the frontend's source lib directory which `lib/` above would
20
+ # otherwise swallow (the original rule was meant for Python virtualenvs).
21
+ !src/frontend/src/lib/
22
+ !src/frontend/src/lib/**
23
  parts/
24
  sdist/
25
  var/
AGENTS.md CHANGED
@@ -1,92 +1,92 @@
1
- # AGENTS.md
2
-
3
- This file defines repository-wide agent guidance for contributors working in this codebase.
4
-
5
- ## Scope And Priority
6
-
7
- - Use this file for shared workflow rules and contributor expectations.
8
- - Use [.github/copilot-instructions.md](.github/copilot-instructions.md) for architecture details, data-flow patterns, and domain-specific constraints.
9
- - If guidance conflicts, prioritize repository reality in this order:
10
- 1. Executable code in [src/ai_agent/](src/ai_agent)
11
- 2. Runtime/config files ([pyproject.toml](pyproject.toml), [config.yaml](config.yaml), [.devcontainer/devcontainer.json](.devcontainer/devcontainer.json))
12
- 3. Documentation pages
13
-
14
- ## Default Execution Context
15
-
16
- Assume work is done inside the dev container unless a task says otherwise.
17
-
18
- - Base environment: Debian Bookworm dev container
19
- - Python: 3.12 (from dev container image)
20
- - Package workflow: uv-managed virtual environment in .venv
21
- - Default interpreter: .venv/bin/python
22
-
23
- Preferred setup and install commands:
24
-
25
- ```bash
26
- uv venv
27
- uv pip install -e .
28
- uv pip install -e ".[dev]"
29
- ```
30
-
31
- ## Command Truth
32
-
33
- Use these commands as the current baseline:
34
-
35
- ```bash
36
- ai_agent chat
37
- ai_agent sync
38
- pytest tests/
39
- ```
40
-
41
- Notes:
42
- - The CLI modes are defined in [src/ai_agent/cli.py](src/ai_agent/cli.py).
43
- - If helper scripts (for example [justfile](justfile)) disagree with CLI behavior, align docs to real CLI behavior and then fix scripts in a follow-up change.
44
-
45
- ## Contributor Workflow Expectations
46
-
47
- 1. Confirm behavior from code before updating docs.
48
- 2. Keep module boundaries clear:
49
- - retrieval logic in `retriever/`
50
- - selection schemas/prompts in `generator/`
51
- - orchestration in `api/`
52
- - chat/tool orchestration in `agent/`
53
- - UI code in `ui/`
54
- 3. Prefer small, reviewable changes.
55
- 4. For user-facing changes, update [CHANGELOG.md](CHANGELOG.md).
56
- 5. For docs changes, keep [README.md](README.md), [docs/index.md](docs/index.md), and [docs/guide.md](docs/guide.md) in sync.
57
-
58
- ## Documentation Maintenance Rules
59
-
60
- When adding or changing functionality:
61
-
62
- 1. Update architectural context if module boundaries change.
63
- 2. Update environment/command docs if startup, install, or test commands change.
64
- 3. Add or adjust examples when behavior changes.
65
- 4. Verify internal links in docs remain valid.
66
-
67
- ## Recommended Improvement Priorities
68
-
69
- - Align task runners/scripts with current CLI contract (`chat`, `sync`).
70
- - Expand tests around UI handlers and tool-call edge cases.
71
- - Add lightweight retrieval quality regression checks.
72
- - Add docs link validation in CI to prevent drift.
73
-
74
-
75
- ## Tests
76
-
77
- 1. Run all tests compatible with pytest in ./tests
78
-
79
- ## Developing flow
80
-
81
- 1. Check AGENTS.md
82
- 2. Follow the implementation
83
- 3. Run tests
84
- 4. Run linting
85
- 5. CHANGELOG upload following keepachangelog format.
86
-
87
- ## Related References
88
-
89
- - [docs/guide.md](docs/guide.md)
90
- - [docs/development/structure.md](docs/development/structure.md)
91
- - [docs/architecture/overview.md](docs/architecture/overview.md)
92
- - [.github/copilot-instructions.md](.github/copilot-instructions.md)
 
1
+ # AGENTS.md
2
+
3
+ This file defines repository-wide agent guidance for contributors working in this codebase.
4
+
5
+ ## Scope And Priority
6
+
7
+ - Use this file for shared workflow rules and contributor expectations.
8
+ - Use [.github/copilot-instructions.md](.github/copilot-instructions.md) for architecture details, data-flow patterns, and domain-specific constraints.
9
+ - If guidance conflicts, prioritize repository reality in this order:
10
+ 1. Executable code in [src/ai_agent/](src/ai_agent)
11
+ 2. Runtime/config files ([pyproject.toml](pyproject.toml), [config.yaml](config.yaml), [.devcontainer/devcontainer.json](.devcontainer/devcontainer.json))
12
+ 3. Documentation pages
13
+
14
+ ## Default Execution Context
15
+
16
+ Assume work is done inside the dev container unless a task says otherwise.
17
+
18
+ - Base environment: Debian Bookworm dev container
19
+ - Python: 3.12 (from dev container image)
20
+ - Package workflow: uv-managed virtual environment in .venv
21
+ - Default interpreter: .venv/bin/python
22
+
23
+ Preferred setup and install commands:
24
+
25
+ ```bash
26
+ uv venv
27
+ uv pip install -e .
28
+ uv pip install -e ".[dev]"
29
+ ```
30
+
31
+ ## Command Truth
32
+
33
+ Use these commands as the current baseline:
34
+
35
+ ```bash
36
+ ai_agent chat
37
+ ai_agent sync
38
+ pytest tests/
39
+ ```
40
+
41
+ Notes:
42
+ - The CLI modes are defined in [src/ai_agent/cli.py](src/ai_agent/cli.py).
43
+ - If helper scripts (for example [justfile](justfile)) disagree with CLI behavior, align docs to real CLI behavior and then fix scripts in a follow-up change.
44
+
45
+ ## Contributor Workflow Expectations
46
+
47
+ 1. Confirm behavior from code before updating docs.
48
+ 2. Keep module boundaries clear:
49
+ - retrieval logic in `retriever/`
50
+ - selection schemas/prompts in `generator/`
51
+ - orchestration in `api/`
52
+ - chat/tool orchestration in `agent/`
53
+ - UI code in `ui/`
54
+ 3. Prefer small, reviewable changes.
55
+ 4. For user-facing changes, update [CHANGELOG.md](CHANGELOG.md).
56
+ 5. For docs changes, keep [README.md](README.md), [docs/index.md](docs/index.md), and [docs/guide.md](docs/guide.md) in sync.
57
+
58
+ ## Documentation Maintenance Rules
59
+
60
+ When adding or changing functionality:
61
+
62
+ 1. Update architectural context if module boundaries change.
63
+ 2. Update environment/command docs if startup, install, or test commands change.
64
+ 3. Add or adjust examples when behavior changes.
65
+ 4. Verify internal links in docs remain valid.
66
+
67
+ ## Recommended Improvement Priorities
68
+
69
+ - Align task runners/scripts with current CLI contract (`chat`, `sync`).
70
+ - Expand tests around UI handlers and tool-call edge cases.
71
+ - Add lightweight retrieval quality regression checks.
72
+ - Add docs link validation in CI to prevent drift.
73
+
74
+
75
+ ## Tests
76
+
77
+ 1. Run all tests compatible with pytest in ./tests
78
+
79
+ ## Developing flow
80
+
81
+ 1. Check AGENTS.md
82
+ 2. Follow the implementation
83
+ 3. Run tests
84
+ 4. Run linting
85
+ 5. CHANGELOG upload following keepachangelog format.
86
+
87
+ ## Related References
88
+
89
+ - [docs/guide.md](docs/guide.md)
90
+ - [docs/development/structure.md](docs/development/structure.md)
91
+ - [docs/architecture/overview.md](docs/architecture/overview.md)
92
+ - [.github/copilot-instructions.md](.github/copilot-instructions.md)
CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
 
3
  All notable changes to this project will be documented in this file.
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  ## [1.0.0]
6
 
7
  ### 🚀 Major Features
 
2
 
3
  All notable changes to this project will be documented in this file.
4
 
5
+ ## [Unreleased]
6
+
7
+ ### Changed
8
+ - Replaced all in-memory caches (image metadata, preview, repo info) with a
9
+ shared SQLite-backed `CacheDB` (`utils/cache_db.py`). Caches now survive
10
+ short process restarts and share a single on-disk file in Python's temp
11
+ directory (`tempfile.gettempdir()`), named `ai_agent_cache{_uid}.db`
12
+ (for example, `/tmp/ai_agent_cache_1000.db`), overridable via
13
+ `CACHE_DB_PATH`.
14
+
15
+ ### Added
16
+ - `utils/shutdown.py`: background cleanup thread that runs immediately on
17
+ startup and then every `CLEANUP_INTERVAL_SECONDS` (default 7200 s):
18
+ - Sweeps expired rows from the cache DB.
19
+ - Deletes log files under `LOG_DIR` older than `LOG_RETENTION_DAYS`
20
+ (default 7 days); only `app_*.log*` files are touched.
21
+ - `atexit` hook performs a final VACUUM + connection close on process exit.
22
+ - New env vars: `CACHE_DB_PATH`, `CLEANUP_INTERVAL_SECONDS`, `LOG_RETENTION_DAYS`.
23
+
24
+ ---
25
+
26
  ## [1.0.0]
27
 
28
  ### 🚀 Major Features
Dockerfile CHANGED
@@ -1,33 +1,41 @@
1
- # Dockerfile at repo root
2
-
3
- # 1. Base image
4
- FROM python:3.11-slim
5
-
6
- # 2. (Optional but useful) system deps for building wheels etc.
7
- RUN apt-get update && apt-get install -y --no-install-recommends \
8
- build-essential git \
9
- && rm -rf /var/lib/apt/lists/*
10
-
11
- # 3. Create non-root user as HF recommends
12
- RUN useradd -m -u 1000 user
13
- USER user
14
-
15
- # 4. Basic env + working dir
16
- ENV HOME=/home/user \
17
- PATH=/home/user/.local/bin:$PATH
18
- WORKDIR $HOME/app
19
-
20
- # 5. Copy project code into the image
21
- COPY --chown=user . .
22
-
23
- # 6. Install Python deps + your package so the `ai_agent` CLI exists
24
- # If you normally do `pip install -e .` locally, this is the Docker equivalent.
25
- RUN pip install --upgrade pip && \
26
- pip install --no-cache-dir .
27
-
28
- # 7. Expose the port the app will listen on
29
- EXPOSE 7860
30
- ENV PORT=7860
31
-
32
- # 8. Start agent
33
- CMD ["ai_agent", "chat"]
 
 
 
 
 
 
 
 
 
1
+ # Multi-stage build:
2
+ # 1. node:20-alpine compiles the Vite/React frontend at src/frontend
3
+ # 2. python:3.11-slim installs the package and runs the FastAPI backend,
4
+ # which also serves the built bundle from /home/user/app/src/frontend/dist.
5
+
6
+ # ---- Stage 1: frontend build ----
7
+ FROM node:20-alpine AS frontend-build
8
+ WORKDIR /app
9
+ COPY src/frontend/package.json src/frontend/package-lock.json ./
10
+ RUN npm ci --no-audit --no-fund
11
+ COPY src/frontend ./
12
+ RUN npm run build
13
+
14
+ # ---- Stage 2: python runtime ----
15
+ FROM python:3.11-slim
16
+
17
+ RUN apt-get update && apt-get install -y --no-install-recommends \
18
+ build-essential git \
19
+ && rm -rf /var/lib/apt/lists/*
20
+
21
+ RUN useradd -m -u 1000 user
22
+ USER user
23
+
24
+ ENV HOME=/home/user \
25
+ PATH=/home/user/.local/bin:$PATH
26
+ WORKDIR $HOME/app
27
+
28
+ COPY --chown=user . .
29
+ COPY --from=frontend-build --chown=user /app/dist ./src/frontend/dist
30
+
31
+ RUN pip install --upgrade pip && \
32
+ pip install --no-cache-dir .
33
+
34
+ EXPOSE 7860
35
+ ENV PORT=7860 \
36
+ HOST=0.0.0.0 \
37
+ FRONTEND_DIST_DIR=src/frontend/dist
38
+
39
+ # Run the FastAPI backend (which also serves the SPA). To fall back to the
40
+ # legacy Gradio UI, override CMD: `docker run ... ai_agent chat`.
41
+ CMD ["ai_agent", "serve"]
LICENSE CHANGED
@@ -1,202 +1,202 @@
1
-
2
- Apache License
3
- Version 2.0, January 2004
4
- http://www.apache.org/licenses/
5
-
6
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
-
8
- 1. Definitions.
9
-
10
- "License" shall mean the terms and conditions for use, reproduction,
11
- and distribution as defined by Sections 1 through 9 of this document.
12
-
13
- "Licensor" shall mean the copyright owner or entity authorized by
14
- the copyright owner that is granting the License.
15
-
16
- "Legal Entity" shall mean the union of the acting entity and all
17
- other entities that control, are controlled by, or are under common
18
- control with that entity. For the purposes of this definition,
19
- "control" means (i) the power, direct or indirect, to cause the
20
- direction or management of such entity, whether by contract or
21
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
- outstanding shares, or (iii) beneficial ownership of such entity.
23
-
24
- "You" (or "Your") shall mean an individual or Legal Entity
25
- exercising permissions granted by this License.
26
-
27
- "Source" form shall mean the preferred form for making modifications,
28
- including but not limited to software source code, documentation
29
- source, and configuration files.
30
-
31
- "Object" form shall mean any form resulting from mechanical
32
- transformation or translation of a Source form, including but
33
- not limited to compiled object code, generated documentation,
34
- and conversions to other media types.
35
-
36
- "Work" shall mean the work of authorship, whether in Source or
37
- Object form, made available under the License, as indicated by a
38
- copyright notice that is included in or attached to the work
39
- (an example is provided in the Appendix below).
40
-
41
- "Derivative Works" shall mean any work, whether in Source or Object
42
- form, that is based on (or derived from) the Work and for which the
43
- editorial revisions, annotations, elaborations, or other modifications
44
- represent, as a whole, an original work of authorship. For the purposes
45
- of this License, Derivative Works shall not include works that remain
46
- separable from, or merely link (or bind by name) to the interfaces of,
47
- the Work and Derivative Works thereof.
48
-
49
- "Contribution" shall mean any work of authorship, including
50
- the original version of the Work and any modifications or additions
51
- to that Work or Derivative Works thereof, that is intentionally
52
- submitted to Licensor for inclusion in the Work by the copyright owner
53
- or by an individual or Legal Entity authorized to submit on behalf of
54
- the copyright owner. For the purposes of this definition, "submitted"
55
- means any form of electronic, verbal, or written communication sent
56
- to the Licensor or its representatives, including but not limited to
57
- communication on electronic mailing lists, source code control systems,
58
- and issue tracking systems that are managed by, or on behalf of, the
59
- Licensor for the purpose of discussing and improving the Work, but
60
- excluding communication that is conspicuously marked or otherwise
61
- designated in writing by the copyright owner as "Not a Contribution."
62
-
63
- "Contributor" shall mean Licensor and any individual or Legal Entity
64
- on behalf of whom a Contribution has been received by Licensor and
65
- subsequently incorporated within the Work.
66
-
67
- 2. Grant of Copyright License. Subject to the terms and conditions of
68
- this License, each Contributor hereby grants to You a perpetual,
69
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
- copyright license to reproduce, prepare Derivative Works of,
71
- publicly display, publicly perform, sublicense, and distribute the
72
- Work and such Derivative Works in Source or Object form.
73
-
74
- 3. Grant of Patent License. Subject to the terms and conditions of
75
- this License, each Contributor hereby grants to You a perpetual,
76
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
- (except as stated in this section) patent license to make, have made,
78
- use, offer to sell, sell, import, and otherwise transfer the Work,
79
- where such license applies only to those patent claims licensable
80
- by such Contributor that are necessarily infringed by their
81
- Contribution(s) alone or by combination of their Contribution(s)
82
- with the Work to which such Contribution(s) was submitted. If You
83
- institute patent litigation against any entity (including a
84
- cross-claim or counterclaim in a lawsuit) alleging that the Work
85
- or a Contribution incorporated within the Work constitutes direct
86
- or contributory patent infringement, then any patent licenses
87
- granted to You under this License for that Work shall terminate
88
- as of the date such litigation is filed.
89
-
90
- 4. Redistribution. You may reproduce and distribute copies of the
91
- Work or Derivative Works thereof in any medium, with or without
92
- modifications, and in Source or Object form, provided that You
93
- meet the following conditions:
94
-
95
- (a) You must give any other recipients of the Work or
96
- Derivative Works a copy of this License; and
97
-
98
- (b) You must cause any modified files to carry prominent notices
99
- stating that You changed the files; and
100
-
101
- (c) You must retain, in the Source form of any Derivative Works
102
- that You distribute, all copyright, patent, trademark, and
103
- attribution notices from the Source form of the Work,
104
- excluding those notices that do not pertain to any part of
105
- the Derivative Works; and
106
-
107
- (d) If the Work includes a "NOTICE" text file as part of its
108
- distribution, then any Derivative Works that You distribute must
109
- include a readable copy of the attribution notices contained
110
- within such NOTICE file, excluding those notices that do not
111
- pertain to any part of the Derivative Works, in at least one
112
- of the following places: within a NOTICE text file distributed
113
- as part of the Derivative Works; within the Source form or
114
- documentation, if provided along with the Derivative Works; or,
115
- within a display generated by the Derivative Works, if and
116
- wherever such third-party notices normally appear. The contents
117
- of the NOTICE file are for informational purposes only and
118
- do not modify the License. You may add Your own attribution
119
- notices within Derivative Works that You distribute, alongside
120
- or as an addendum to the NOTICE text from the Work, provided
121
- that such additional attribution notices cannot be construed
122
- as modifying the License.
123
-
124
- You may add Your own copyright statement to Your modifications and
125
- may provide additional or different license terms and conditions
126
- for use, reproduction, or distribution of Your modifications, or
127
- for any such Derivative Works as a whole, provided Your use,
128
- reproduction, and distribution of the Work otherwise complies with
129
- the conditions stated in this License.
130
-
131
- 5. Submission of Contributions. Unless You explicitly state otherwise,
132
- any Contribution intentionally submitted for inclusion in the Work
133
- by You to the Licensor shall be under the terms and conditions of
134
- this License, without any additional terms or conditions.
135
- Notwithstanding the above, nothing herein shall supersede or modify
136
- the terms of any separate license agreement you may have executed
137
- with Licensor regarding such Contributions.
138
-
139
- 6. Trademarks. This License does not grant permission to use the trade
140
- names, trademarks, service marks, or product names of the Licensor,
141
- except as required for reasonable and customary use in describing the
142
- origin of the Work and reproducing the content of the NOTICE file.
143
-
144
- 7. Disclaimer of Warranty. Unless required by applicable law or
145
- agreed to in writing, Licensor provides the Work (and each
146
- Contributor provides its Contributions) on an "AS IS" BASIS,
147
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
- implied, including, without limitation, any warranties or conditions
149
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
- PARTICULAR PURPOSE. You are solely responsible for determining the
151
- appropriateness of using or redistributing the Work and assume any
152
- risks associated with Your exercise of permissions under this License.
153
-
154
- 8. Limitation of Liability. In no event and under no legal theory,
155
- whether in tort (including negligence), contract, or otherwise,
156
- unless required by applicable law (such as deliberate and grossly
157
- negligent acts) or agreed to in writing, shall any Contributor be
158
- liable to You for damages, including any direct, indirect, special,
159
- incidental, or consequential damages of any character arising as a
160
- result of this License or out of the use or inability to use the
161
- Work (including but not limited to damages for loss of goodwill,
162
- work stoppage, computer failure or malfunction, or any and all
163
- other commercial damages or losses), even if such Contributor
164
- has been advised of the possibility of such damages.
165
-
166
- 9. Accepting Warranty or Additional Liability. While redistributing
167
- the Work or Derivative Works thereof, You may choose to offer,
168
- and charge a fee for, acceptance of support, warranty, indemnity,
169
- or other liability obligations and/or rights consistent with this
170
- License. However, in accepting such obligations, You may act only
171
- on Your own behalf and on Your sole responsibility, not on behalf
172
- of any other Contributor, and only if You agree to indemnify,
173
- defend, and hold each Contributor harmless for any liability
174
- incurred by, or claims asserted against, such Contributor by reason
175
- of your accepting any such warranty or additional liability.
176
-
177
- END OF TERMS AND CONDITIONS
178
-
179
- APPENDIX: How to apply the Apache License to your work.
180
-
181
- To apply the Apache License to your work, attach the following
182
- boilerplate notice, with the fields enclosed by brackets "[]"
183
- replaced with your own identifying information. (Don't include
184
- the brackets!) The text should be enclosed in the appropriate
185
- comment syntax for the file format. We also recommend that a
186
- file or class name and description of purpose be included on the
187
- same "printed page" as the copyright notice for easier
188
- identification within third-party archives.
189
-
190
- Copyright 2026 Imaging Plaza
191
-
192
- Licensed under the Apache License, Version 2.0 (the "License");
193
- you may not use this file except in compliance with the License.
194
- You may obtain a copy of the License at
195
-
196
- http://www.apache.org/licenses/LICENSE-2.0
197
-
198
- Unless required by applicable law or agreed to in writing, software
199
- distributed under the License is distributed on an "AS IS" BASIS,
200
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
- See the License for the specific language governing permissions and
202
  limitations under the License.
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright 2026 Imaging Plaza
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
  limitations under the License.
README.md CHANGED
@@ -1,562 +1,562 @@
1
- ---
2
- title: AI Agent
3
- emoji: 🤖
4
- colorFrom: blue
5
- colorTo: green
6
- sdk: docker
7
- app_port: 7860
8
- pinned: false
9
- ---
10
-
11
- # AI Imaging Agent (Imaging Plaza)
12
-
13
- [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
14
- [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
15
-
16
- An intelligent RAG + AI agent system that helps users discover the right imaging software for their images and tasks. Upload an image, describe what you want to do, and get ranked tool recommendations with links to runnable demos.
17
-
18
- ## ✨ Key Features
19
-
20
- - **🤖 Conversational AI Agent**: Natural language interaction with multi-turn context
21
- - **🔍 Smart Retrieval**: BGE-M3 embeddings + FAISS + CrossEncoder reranking
22
- - **👁️ Vision-Aware Selection**: VLM-based tool selection considering both image content and metadata
23
- - **🏥 Medical Imaging Focus**: Specialized support for CT, MRI, DICOM, NIfTI, and other medical formats
24
- - **🎯 Format-Aware Matching**: IO compatibility scoring based on file formats and dimensions
25
- - **🚀 Demo Integration**: Direct execution of Gradio Space demos on your images
26
- - **📊 Rich UI**: Chat interface with image previews, file management, and execution traces
27
-
28
- <p align="center">
29
- <img src="https://github.com/Imaging-Plaza/ai-agent/blob/develop/assets/example.gif?raw=true" height="700">
30
- </p>
31
-
32
- ---
33
-
34
- ## 🚀 Quick Start
35
-
36
- ### Prerequisites
37
-
38
- - Python 3.10–3.12
39
- - OpenAI API key (or compatible API endpoint)
40
- - Internet connection for model calls
41
-
42
- ### Installation
43
-
44
- ```bash
45
- # Clone the repository
46
- git clone <your-repo-url>
47
- cd ai-agent
48
-
49
- # Create virtual environment
50
- python -m venv .venv
51
-
52
- # Activate virtual environment
53
- # On Linux/macOS:
54
- source .venv/bin/activate
55
- # On Windows:
56
- .venv\Scripts\activate
57
-
58
- # Install the package
59
- pip install --upgrade pip
60
- pip install -e .
61
-
62
- # For development (includes test dependencies)
63
- pip install -e ".[dev]"
64
- ```
65
-
66
- ### Configuration
67
-
68
- Create a `.env` file at the repository root:
69
-
70
- ```dotenv
71
- # Required: OpenAI API key
72
- OPENAI_API_KEY=sk-xxxx
73
-
74
- # Optional: GitHub token for repo info tool
75
- GITHUB_TOKEN=ghp_xxxx
76
-
77
- # Optional: Alternative model providers (EPFL, etc.)
78
- EPFL_API_KEY=sk-xxxx
79
- EPFL_API_KEY_EMBEDDER=sk-xxxx
80
-
81
- # Software catalog path
82
- SOFTWARE_CATALOG=dataset/catalog.jsonl
83
-
84
- # Pipeline configuration
85
- TOP_K=8 # Number of candidates to retrieve
86
- NUM_CHOICES=3 # Number of tools to recommend
87
- AGENT_OUTPUT_RETRIES=3 # Structured output validation retries
88
- EMBED_CATALOG_ON_START=1 # Pre-embed catalog if FAISS is empty
89
-
90
- # Logging configuration
91
- LOGLEVEL_CONSOLE=WARNING
92
- LOGLEVEL_FILE=INFO
93
- FILE_LOG=1
94
- LOG_DIR=logs
95
- LOG_PROMPTS=0 # Set to 1 to save prompt snapshots for debugging
96
-
97
- # Custom config path
98
- CONFIG_PATH=config.yaml
99
- ```
100
-
101
- ### Model Configuration
102
-
103
- The agent model can be configured via `config.yaml`:
104
-
105
- ```yaml
106
- # AI Agent Model Configuration
107
-
108
- # Default/fallback model (used for CLI and initial startup)
109
- agent_model:
110
- name: "gpt-5.1"
111
- base_url: null # null for default OpenAI endpoint
112
- api_key_env: "OPENAI_API_KEY"
113
-
114
- # Available models for UI dropdown
115
- available_models:
116
- - display_name: "gpt-4o-mini"
117
- name: "gpt-4o-mini"
118
- base_url: null
119
- provider: "OpenAI"
120
- api_key_env: "OPENAI_API_KEY"
121
-
122
- - display_name: "gpt-4o"
123
- name: "gpt-4o"
124
- base_url: null
125
- provider: "OpenAI"
126
- api_key_env: "OPENAI_API_KEY"
127
-
128
- - display_name: "gpt-5-mini"
129
- name: "gpt-5-mini"
130
- base_url: null
131
- provider: "OpenAI"
132
- api_key_env: "OPENAI_API_KEY"
133
-
134
- - display_name: "gpt-5.1"
135
- name: "gpt-5.1"
136
- base_url: null
137
- provider: "OpenAI"
138
- api_key_env: "OPENAI_API_KEY"
139
-
140
- retrieval:
141
- embedder:
142
- backend: "remote" # "remote" or "local"
143
- model_name: "Qwen/Qwen3-Embedding-8B"
144
- base_url: "https://inference-rcp.epfl.ch/v1"
145
- api_key_env: "EPFL_API_KEY_EMBEDDER"
146
- timeout_s: 20
147
- # local example:
148
- # backend: "local"
149
- # model_name: "BAAI/bge-m3"
150
- # device: "cpu" # optional
151
-
152
- reranker:
153
- backend: "remote" # "remote" or "local"
154
- model_name: "BAAI/bge-reranker-v2-m3"
155
- base_url: "https://inference-rcp.epfl.ch/v1"
156
- api_key_env: "EPFL_API_KEY_EMBEDDER"
157
- timeout_s: 20
158
- # local example:
159
- # backend: "local"
160
- # model_name: "BAAI/bge-reranker-v2-m3"
161
- # device: "cpu" # optional
162
- ```
163
-
164
- ### Running the App
165
-
166
- ```bash
167
- # Start the chat interface
168
- ai_agent chat
169
-
170
- # Open your browser to:
171
- # http://127.0.0.1:7860
172
- ```
173
-
174
- Try uploading a cat image and asking:
175
- > "I want to segment the cat from this image"
176
-
177
- ---
178
-
179
- ## 💬 Usage
180
-
181
- ### Chat Interface
182
-
183
- The chat interface provides a natural conversation flow:
184
-
185
- 1. **Upload Files**: Drop images (PNG, JPG, TIFF, DICOM, NIfTI, etc.) or other supported files
186
- 2. **Describe Your Task**: Use natural language like "segment the lungs" or "register brain MRI"
187
- 3. **Review Recommendations**: Get ranked tool suggestions with accuracy scores and explanations
188
- 4. **Run Demos**: Click "Run demo" to execute tools directly on your uploaded images
189
- 5. **Iterate**: Ask for alternatives, refine your query, or upload different files
190
-
191
- ### Supported File Formats
192
-
193
- **Images:**
194
- - Standard: PNG, JPG, JPEG, WEBP, BMP, GIF
195
- - Medical: DICOM (.dcm), NIfTI (.nii, .nii.gz), TIFF stacks
196
- - Scientific: Multi-page TIFF, TIFF with metadata
197
-
198
- **Other Files:**
199
- - Data: CSV, JSON, XML
200
- - Media: MP3, MP4
201
-
202
- ### Example Queries
203
-
204
- - "Segment the lungs from this CT scan"
205
- - "Register these two brain MRI images"
206
- - "Extract text from this medical report image"
207
- - "Classify what organ is shown in this ultrasound"
208
- - "Detect tumors in this MRI scan"
209
- - "I need to analyze DICOM files, what tools are available?"
210
-
211
- ### Understanding Results
212
-
213
- Each recommendation includes:
214
- - **Rank**: Priority order (1 = best match)
215
- - **Accuracy Score**: Confidence level (0-100%)
216
- - **Explanation**: Why this tool matches your request
217
- - **Metadata**: Supported modalities, dimensions, formats, license
218
- - **Demo Link**: Direct link to runnable example
219
-
220
- ---
221
-
222
- ## 🏗️ Architecture
223
-
224
- ### Pipeline Overview
225
-
226
- The system follows a two-stage architecture:
227
-
228
- ```
229
- User Input (Image + Text Query)
230
-
231
- ┌───────────────────────────────┐
232
- │ RETRIEVAL STAGE │
233
- │ - BGE-M3 Embeddings │
234
- │ - FAISS Vector Search │
235
- │ - CrossEncoder Reranking │
236
- │ - Format Token Matching │
237
- └───────────────────────────────┘
238
- ↓ Top-K Candidates
239
- ┌───────────────────────────────┐
240
- │ AGENT SELECTION │
241
- │ - Pydantic AI Agent │
242
- │ - OpenAI VLM │
243
- │ - Image + Metadata Analysis │
244
- │ - Multi-Tool Reasoning │
245
- └───────────────────────────────┘
246
-
247
- Ranked Recommendations
248
- ```
249
-
250
- ### Retrieval Stage
251
-
252
- **No LLM calls** - purely text-based search:
253
-
254
- 1. **Query Construction**: User task + format tokens from uploaded files
255
- 2. **Embedding**: BGE-M3 model generates query embedding
256
- 3. **Vector Search**: FAISS retrieves top candidates
257
- 4. **Reranking**: CrossEncoder refines results for precision
258
- 5. **Retry Broadening**: If too few hits, retry with a shorter/broader query
259
-
260
- ### Agent Selection Stage
261
-
262
- **Single VLM call** - multimodal reasoning:
263
-
264
- 1. **Input Preparation**:
265
- - Text: User query + candidate table + file metadata
266
- - Image: PNG preview (converted from any format)
267
- - Context: Original file format, dimensions, modality
268
-
269
- 2. **Agent Tools**:
270
- - `search_tools`: Search catalog with query
271
- - `search_alternative`: Find alternatives (iterative)
272
- - `repo_info`: Fetch GitHub documentation via DeepWiki MCP
273
-
274
- 3. **Output**: Ranked tool selections with accuracy scores and explanations
275
-
276
- ### Key Components
277
-
278
- - **`api/pipeline.py`**: RAG retrieval orchestrator
279
- - **`agent/agent.py`**: Pydantic AI agent with tool definitions
280
- - **`retriever/`**: Embedding, FAISS indexing, reranking
281
- - **`generator/`**: Prompts and schema for tool selection
282
- - **`ui/`**: Gradio chat interface components
283
- - **`utils/`**: Image processing, metadata extraction, file validation
284
- - **`catalog/`**: Catalog syncing from GraphDB (optional)
285
-
286
- ---
287
-
288
- ## ⚙️ Configuration
289
-
290
- ### Environment Variables
291
-
292
- | Variable | Description | Default | Required |
293
- |----------|-------------|---------|----------|
294
- | `OPENAI_API_KEY` | OpenAI API key | - | ✅ |
295
- | `EPFL_API_KEY_EMBEDDER` | API key for remote embedder and reranker endpoints | - | ✅ (when `retrieval.embedder.backend: remote` and/or `retrieval.reranker.backend: remote`) |
296
- | `GITHUB_TOKEN` | GitHub token for repo info | - | ❌ |
297
- | `SOFTWARE_CATALOG` | Path to catalog JSONL | `dataset/catalog.jsonl` | ✅ |
298
- | `TOP_K` | Retrieval candidates count | `8` | ❌ |
299
- | `NUM_CHOICES` | Tools to recommend | `3` | ❌ |
300
- | `AGENT_OUTPUT_RETRIES` | Structured output validation retries | `3` | ❌ |
301
- | `EMBED_CATALOG_ON_START` | Pre-embed catalog on startup when FAISS is empty | `1` | ❌ |
302
- | `LOGLEVEL_CONSOLE` | Console log level | `WARNING` | ❌ |
303
- | `LOGLEVEL_FILE` | File log level | `INFO` | ❌ |
304
- | `FILE_LOG` | Enable file logging | `1` | ❌ |
305
- | `LOG_DIR` | Log directory | `logs` | ❌ |
306
- | `LOG_PROMPTS` | Save prompt snapshots | `0` | ❌ |
307
- | `CONFIG_PATH` | Model config file | `config.yaml` | ✅ |
308
-
309
- ### GraphDB Catalog Sync (Optional)
310
-
311
- For automatic catalog syncing from a GraphDB instance:
312
-
313
- ```dotenv
314
- GRAPHDB_URL=https://your-graphdb.example.com
315
- GRAPHDB_GRAPH=your-graph-name
316
- GRAPHDB_USER=username
317
- GRAPHDB_PASSWORD=password
318
- GRAPHDB_QUERY_FILE=/path/to/query.rq
319
- SYNC_EVERY_HOURS=24 # Auto-refresh interval (0 to disable)
320
- OUTPUT_JSONLD=dataset/catalog.jsonld
321
- OUTPUT_JSONL=dataset/catalog.jsonl
322
- ```
323
-
324
- Run manual sync:
325
- ```bash
326
- ai_agent sync
327
- ```
328
-
329
- ---
330
-
331
- ## 📋 Catalog Format
332
-
333
- The catalog is a JSONL file where each line is a `SoftwareDoc` following schema.org SoftwareSourceCode structure.
334
-
335
- ### Minimal Example
336
-
337
- ```json
338
- {
339
- "name": "3d-lungs-segmentation",
340
- "description": "3D lung segmentation from CT; returns a mask/overlay.",
341
-
342
- "applicationCategory": ["Medical Imaging"],
343
- "featureList": ["segmentation"],
344
- "imagingModality": ["CT"],
345
- "dims": [3],
346
- "anatomy": ["lung"],
347
- "keywords": ["mask", "overlay", "lung segmentation", "CT"],
348
-
349
- "programmingLanguage": "Python",
350
- "requiresGPU": false,
351
- "isAccessibleForFree": true,
352
- "license": "Apache-2.0",
353
-
354
- "supportingData": [
355
- {
356
- "datasetFormat": "TIFF",
357
- "bodySite": "lung",
358
- "imagingModality": "CT",
359
- "hasDimensionality": 3
360
- },
361
- {
362
- "datasetFormat": "DICOM",
363
- "bodySite": "lung",
364
- "imagingModality": "CT",
365
- "hasDimensionality": 3
366
- }
367
- ],
368
-
369
- "runnableExample": [
370
- {
371
- "hostType": "gradio",
372
- "url": "https://huggingface.co/spaces/qchapp/3d-lungs-segmentation",
373
- "name": "HF Space"
374
- }
375
- ]
376
- }
377
- ```
378
-
379
- ### Key Fields
380
-
381
- - **name**: Unique identifier for the tool
382
- - **description**: Clear explanation of what the tool does
383
- - **featureList**: Operations (e.g., segmentation, registration, classification)
384
- - **imagingModality**: Medical imaging types (CT, MRI, XR, US, PET)
385
- - **dims**: Supported dimensions (2D, 3D, 4D)
386
- - **anatomy**: Body parts/organs
387
- - **supportingData**: Format compatibility information (critical for matching)
388
- - **runnableExample**: Links to live demos (HuggingFace Spaces, notebooks, web apps)
389
-
390
- ---
391
-
392
- ## 🔧 Development
393
-
394
- ### Project Structure
395
-
396
- ```
397
- ai-agent/
398
- ├── src/ai_agent/
399
- │ ├── agent/ # Pydantic AI agent and tools
400
- │ │ ├── agent.py # Agent definition
401
- │ │ ├── models.py # Agent state models
402
- │ │ ├── tools/ # Agent tool implementations
403
- │ │ │ ├── search_tool.py
404
- │ │ │ ├── search_alternative_tool.py
405
- │ │ │ ├── gradio_space_tool.py
406
- │ │ │ ├── repo_info_tool.py
407
- │ │ │ └── deepwiki_tool.py
408
- │ │ └── utils.py
409
- │ ├── api/ # Pipeline orchestration
410
- │ │ └── pipeline.py # RAGImagingPipeline
411
- │ ├── retriever/ # Retrieval components
412
- │ │ ├── text_embedder.py
413
- │ │ ├── vector_index.py
414
- │ │ ├── reranker.py
415
- │ │ └── software_doc.py
416
- │ ├── generator/ # Agent prompts and schemas
417
- │ │ ├── prompts.py
418
- │ │ └── schema.py
419
- │ ├── ui/ # Gradio interface
420
- │ │ ├── app.py
421
- │ │ ├── handlers.py
422
- │ │ ├── components.py
423
- │ │ ├── formatters.py
424
- │ │ ├── state.py
425
- │ │ └── visualizations.py
426
- │ ├── utils/ # Shared utilities
427
- │ │ ├── config.py # Configuration management
428
- │ │ ├── file_validator.py
429
- │ │ ├── image_meta.py # Metadata extraction
430
- │ │ ├── image_io.py
431
- │ │ ├── previews.py
432
- │ │ └── tags.py
433
- │ ├── catalog/ # Catalog syncing
434
- │ │ └── sync.py
435
- │ └── cli.py # CLI entry point
436
- ├── tests/ # Test suite
437
- │ ├── test_retrieval_pipeline.py
438
- │ ├── test_repo_summary.py
439
- │ └── data/
440
- ├── artifacts/ # Generated artifacts
441
- │ └── rag_index/ # FAISS index
442
- ├── dataset/ # Catalog data
443
- │ └── catalog.jsonl
444
- ├── logs/ # Application logs
445
- ├── config.yaml # Model configuration
446
- ├── pyproject.toml # Project metadata & dependencies
447
- ├── Dockerfile # Production Docker image
448
- ├── tools/image/Dockerfile # Development Docker image
449
- └── justfile # Task runner commands
450
- ```
451
-
452
- ### Local Development
453
-
454
- ```bash
455
- # Install in development mode
456
- pip install -e ".[dev]"
457
-
458
- # Run tests
459
- pytest tests/
460
- ```
461
-
462
- ### Testing
463
-
464
- Run the test suite:
465
-
466
- ```bash
467
- # All tests
468
- pytest tests/
469
-
470
- # Specific test file
471
- pytest tests/test_retrieval_pipeline.py
472
-
473
- # With verbose output
474
- pytest -v tests/
475
-
476
- # With coverage
477
- pytest --cov=ai_agent tests/
478
- ```
479
-
480
- ### Logging & Debugging
481
-
482
- **Console Logs**: Set `LOGLEVEL_CONSOLE=DEBUG` for verbose output
483
-
484
- **File Logs**: Automatically saved to `logs/app_YYYYMMDD.log` (rotates daily)
485
-
486
- **Prompt Snapshots**: Enable `LOG_PROMPTS=1` to save:
487
- - `logs/vlm_selector_YYYYMMDD_HHMMSS.txt` - System/user prompts
488
-
489
- ---
490
-
491
- ## 📚 API & CLI Reference
492
-
493
- ### CLI Commands
494
-
495
- ```bash
496
- # Launch chat interface
497
- ai_agent chat
498
-
499
- # Sync catalog from GraphDB
500
- ai_agent sync
501
- ```
502
-
503
- ## 🗺️ Maintainer Guide
504
-
505
- For full project documentation with detailed folder responsibilities, environment defaults, and improvement guidelines, see [docs/guide.md](docs/guide.md).
506
-
507
- ---
508
-
509
- ## 📝 Changelog
510
-
511
- See [CHANGELOG.md](CHANGELOG.md) for detailed version history.
512
-
513
- ### Recent Highlights
514
-
515
- **[1.0.0]**
516
- - ✨ New chat-based interface (`ai_agent chat`) with rich media and tool integration
517
- - 🛠️ Fully agent-based architecture replacing legacy pipelines
518
- - 🔍 Smarter retrieval with automatic retry
519
- - 🔗 DeepWiki MCP integration for fast GitHub repository documentation access
520
- - 🔧 YAML configuration (`config.yaml`) for flexible model and backend setup
521
- - 🎨 Redesigned UI with Imaging Plaza branding and improved UX
522
- - ⚡ Performance improvements (pre-embedding, caching, faster startup)
523
- - 🧹 Major cleanup: removed deprecated code paths, legacy UI, and outdated tests
524
-
525
- **[0.1.3] - 2025-10-22**
526
- - Gradio space runner tool
527
- - Repository info tool
528
- - UI fixes and polish
529
-
530
- ---
531
-
532
- ## 📄 License
533
-
534
- This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
535
-
536
- ---
537
-
538
- ## 🙏 Credits & Acknowledgments
539
-
540
- **Developed by**: Imaging Plaza Team
541
-
542
- **Technologies:**
543
- - [Pydantic AI](https://github.com/pydantic/pydantic-ai) - AI agent framework
544
- - [OpenAI](https://openai.com) - GPT vision model
545
- - [FAISS](https://github.com/facebookresearch/faiss) - Vector search
546
- - [BGE-M3](https://huggingface.co/BAAI/bge-m3) - Multilingual embeddings
547
- - [Gradio](https://gradio.app) - Interactive web UI
548
- - [DeepWiki](https://deepwiki.com) - GitHub repository documentation
549
-
550
- **Medical Imaging Formats:**
551
- - [pydicom](https://github.com/pydicom/pydicom) - DICOM support
552
- - [nibabel](https://nipy.org/nibabel/) - NIfTI support
553
-
554
- ---
555
-
556
- ## 📮 Support
557
-
558
- For issues, questions, or contributions, please contact the Imaging Plaza team.
559
-
560
- ---
561
-
562
- **🏥 Medical Disclaimer**: This software is a tool recommendation system, not a diagnostic tool. Always consult qualified medical professionals for clinical decisions.
 
1
+ ---
2
+ title: AI Agent
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # AI Imaging Agent (Imaging Plaza)
12
+
13
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
14
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
15
+
16
+ An intelligent RAG + AI agent system that helps users discover the right imaging software for their images and tasks. Upload an image, describe what you want to do, and get ranked tool recommendations with links to runnable demos.
17
+
18
+ ## ✨ Key Features
19
+
20
+ - **🤖 Conversational AI Agent**: Natural language interaction with multi-turn context
21
+ - **🔍 Smart Retrieval**: BGE-M3 embeddings + FAISS + CrossEncoder reranking
22
+ - **👁️ Vision-Aware Selection**: VLM-based tool selection considering both image content and metadata
23
+ - **🏥 Medical Imaging Focus**: Specialized support for CT, MRI, DICOM, NIfTI, and other medical formats
24
+ - **🎯 Format-Aware Matching**: IO compatibility scoring based on file formats and dimensions
25
+ - **🚀 Demo Integration**: Direct execution of Gradio Space demos on your images
26
+ - **📊 Rich UI**: Chat interface with image previews, file management, and execution traces
27
+
28
+ <p align="center">
29
+ <img src="https://github.com/Imaging-Plaza/ai-agent/blob/develop/assets/example.gif?raw=true" height="700">
30
+ </p>
31
+
32
+ ---
33
+
34
+ ## 🚀 Quick Start
35
+
36
+ ### Prerequisites
37
+
38
+ - Python 3.10–3.12
39
+ - OpenAI API key (or compatible API endpoint)
40
+ - Internet connection for model calls
41
+
42
+ ### Installation
43
+
44
+ ```bash
45
+ # Clone the repository
46
+ git clone <your-repo-url>
47
+ cd ai-agent
48
+
49
+ # Create virtual environment
50
+ python -m venv .venv
51
+
52
+ # Activate virtual environment
53
+ # On Linux/macOS:
54
+ source .venv/bin/activate
55
+ # On Windows:
56
+ .venv\Scripts\activate
57
+
58
+ # Install the package
59
+ pip install --upgrade pip
60
+ pip install -e .
61
+
62
+ # For development (includes test dependencies)
63
+ pip install -e ".[dev]"
64
+ ```
65
+
66
+ ### Configuration
67
+
68
+ Create a `.env` file at the repository root:
69
+
70
+ ```dotenv
71
+ # Required: OpenAI API key
72
+ OPENAI_API_KEY=sk-xxxx
73
+
74
+ # Optional: GitHub token for repo info tool
75
+ GITHUB_TOKEN=ghp_xxxx
76
+
77
+ # Optional: Alternative model providers (EPFL, etc.)
78
+ EPFL_API_KEY=sk-xxxx
79
+ EPFL_API_KEY_EMBEDDER=sk-xxxx
80
+
81
+ # Software catalog path
82
+ SOFTWARE_CATALOG=dataset/catalog.jsonl
83
+
84
+ # Pipeline configuration
85
+ TOP_K=8 # Number of candidates to retrieve
86
+ NUM_CHOICES=3 # Number of tools to recommend
87
+ AGENT_OUTPUT_RETRIES=3 # Structured output validation retries
88
+ EMBED_CATALOG_ON_START=1 # Pre-embed catalog if FAISS is empty
89
+
90
+ # Logging configuration
91
+ LOGLEVEL_CONSOLE=WARNING
92
+ LOGLEVEL_FILE=INFO
93
+ FILE_LOG=1
94
+ LOG_DIR=logs
95
+ LOG_PROMPTS=0 # Set to 1 to save prompt snapshots for debugging
96
+
97
+ # Custom config path
98
+ CONFIG_PATH=config.yaml
99
+ ```
100
+
101
+ ### Model Configuration
102
+
103
+ The agent model can be configured via `config.yaml`:
104
+
105
+ ```yaml
106
+ # AI Agent Model Configuration
107
+
108
+ # Default/fallback model (used for CLI and initial startup)
109
+ agent_model:
110
+ name: "gpt-5.1"
111
+ base_url: null # null for default OpenAI endpoint
112
+ api_key_env: "OPENAI_API_KEY"
113
+
114
+ # Available models for UI dropdown
115
+ available_models:
116
+ - display_name: "gpt-4o-mini"
117
+ name: "gpt-4o-mini"
118
+ base_url: null
119
+ provider: "OpenAI"
120
+ api_key_env: "OPENAI_API_KEY"
121
+
122
+ - display_name: "gpt-4o"
123
+ name: "gpt-4o"
124
+ base_url: null
125
+ provider: "OpenAI"
126
+ api_key_env: "OPENAI_API_KEY"
127
+
128
+ - display_name: "gpt-5-mini"
129
+ name: "gpt-5-mini"
130
+ base_url: null
131
+ provider: "OpenAI"
132
+ api_key_env: "OPENAI_API_KEY"
133
+
134
+ - display_name: "gpt-5.1"
135
+ name: "gpt-5.1"
136
+ base_url: null
137
+ provider: "OpenAI"
138
+ api_key_env: "OPENAI_API_KEY"
139
+
140
+ retrieval:
141
+ embedder:
142
+ backend: "remote" # "remote" or "local"
143
+ model_name: "Qwen/Qwen3-Embedding-8B"
144
+ base_url: "https://inference-rcp.epfl.ch/v1"
145
+ api_key_env: "EPFL_API_KEY_EMBEDDER"
146
+ timeout_s: 20
147
+ # local example:
148
+ # backend: "local"
149
+ # model_name: "BAAI/bge-m3"
150
+ # device: "cpu" # optional
151
+
152
+ reranker:
153
+ backend: "remote" # "remote" or "local"
154
+ model_name: "BAAI/bge-reranker-v2-m3"
155
+ base_url: "https://inference-rcp.epfl.ch/v1"
156
+ api_key_env: "EPFL_API_KEY_EMBEDDER"
157
+ timeout_s: 20
158
+ # local example:
159
+ # backend: "local"
160
+ # model_name: "BAAI/bge-reranker-v2-m3"
161
+ # device: "cpu" # optional
162
+ ```
163
+
164
+ ### Running the App
165
+
166
+ ```bash
167
+ # Start the chat interface
168
+ ai_agent chat
169
+
170
+ # Open your browser to:
171
+ # http://127.0.0.1:7860
172
+ ```
173
+
174
+ Try uploading a cat image and asking:
175
+ > "I want to segment the cat from this image"
176
+
177
+ ---
178
+
179
+ ## 💬 Usage
180
+
181
+ ### Chat Interface
182
+
183
+ The chat interface provides a natural conversation flow:
184
+
185
+ 1. **Upload Files**: Drop images (PNG, JPG, TIFF, DICOM, NIfTI, etc.) or other supported files
186
+ 2. **Describe Your Task**: Use natural language like "segment the lungs" or "register brain MRI"
187
+ 3. **Review Recommendations**: Get ranked tool suggestions with accuracy scores and explanations
188
+ 4. **Run Demos**: Click "Run demo" to execute tools directly on your uploaded images
189
+ 5. **Iterate**: Ask for alternatives, refine your query, or upload different files
190
+
191
+ ### Supported File Formats
192
+
193
+ **Images:**
194
+ - Standard: PNG, JPG, JPEG, WEBP, BMP, GIF
195
+ - Medical: DICOM (.dcm), NIfTI (.nii, .nii.gz), TIFF stacks
196
+ - Scientific: Multi-page TIFF, TIFF with metadata
197
+
198
+ **Other Files:**
199
+ - Data: CSV, JSON, XML
200
+ - Media: MP3, MP4
201
+
202
+ ### Example Queries
203
+
204
+ - "Segment the lungs from this CT scan"
205
+ - "Register these two brain MRI images"
206
+ - "Extract text from this medical report image"
207
+ - "Classify what organ is shown in this ultrasound"
208
+ - "Detect tumors in this MRI scan"
209
+ - "I need to analyze DICOM files, what tools are available?"
210
+
211
+ ### Understanding Results
212
+
213
+ Each recommendation includes:
214
+ - **Rank**: Priority order (1 = best match)
215
+ - **Accuracy Score**: Confidence level (0-100%)
216
+ - **Explanation**: Why this tool matches your request
217
+ - **Metadata**: Supported modalities, dimensions, formats, license
218
+ - **Demo Link**: Direct link to runnable example
219
+
220
+ ---
221
+
222
+ ## 🏗️ Architecture
223
+
224
+ ### Pipeline Overview
225
+
226
+ The system follows a two-stage architecture:
227
+
228
+ ```
229
+ User Input (Image + Text Query)
230
+
231
+ ┌───────────────────────────────┐
232
+ │ RETRIEVAL STAGE │
233
+ │ - BGE-M3 Embeddings │
234
+ │ - FAISS Vector Search │
235
+ │ - CrossEncoder Reranking │
236
+ │ - Format Token Matching │
237
+ └───────────────────────────────┘
238
+ ↓ Top-K Candidates
239
+ ┌───────────────────────────────┐
240
+ │ AGENT SELECTION │
241
+ │ - Pydantic AI Agent │
242
+ │ - OpenAI VLM │
243
+ │ - Image + Metadata Analysis │
244
+ │ - Multi-Tool Reasoning │
245
+ └───────────────────────────────┘
246
+
247
+ Ranked Recommendations
248
+ ```
249
+
250
+ ### Retrieval Stage
251
+
252
+ **No LLM calls** - purely text-based search:
253
+
254
+ 1. **Query Construction**: User task + format tokens from uploaded files
255
+ 2. **Embedding**: BGE-M3 model generates query embedding
256
+ 3. **Vector Search**: FAISS retrieves top candidates
257
+ 4. **Reranking**: CrossEncoder refines results for precision
258
+ 5. **Retry Broadening**: If too few hits, retry with a shorter/broader query
259
+
260
+ ### Agent Selection Stage
261
+
262
+ **Single VLM call** - multimodal reasoning:
263
+
264
+ 1. **Input Preparation**:
265
+ - Text: User query + candidate table + file metadata
266
+ - Image: PNG preview (converted from any format)
267
+ - Context: Original file format, dimensions, modality
268
+
269
+ 2. **Agent Tools**:
270
+ - `search_tools`: Search catalog with query
271
+ - `search_alternative`: Find alternatives (iterative)
272
+ - `repo_info`: Fetch GitHub documentation via DeepWiki MCP
273
+
274
+ 3. **Output**: Ranked tool selections with accuracy scores and explanations
275
+
276
+ ### Key Components
277
+
278
+ - **`api/pipeline.py`**: RAG retrieval orchestrator
279
+ - **`agent/agent.py`**: Pydantic AI agent with tool definitions
280
+ - **`retriever/`**: Embedding, FAISS indexing, reranking
281
+ - **`generator/`**: Prompts and schema for tool selection
282
+ - **`ui/`**: Gradio chat interface components
283
+ - **`utils/`**: Image processing, metadata extraction, file validation
284
+ - **`catalog/`**: Catalog syncing from GraphDB (optional)
285
+
286
+ ---
287
+
288
+ ## ⚙️ Configuration
289
+
290
+ ### Environment Variables
291
+
292
+ | Variable | Description | Default | Required |
293
+ |----------|-------------|---------|----------|
294
+ | `OPENAI_API_KEY` | OpenAI API key | - | ✅ |
295
+ | `EPFL_API_KEY_EMBEDDER` | API key for remote embedder and reranker endpoints | - | ✅ (when `retrieval.embedder.backend: remote` and/or `retrieval.reranker.backend: remote`) |
296
+ | `GITHUB_TOKEN` | GitHub token for repo info | - | ❌ |
297
+ | `SOFTWARE_CATALOG` | Path to catalog JSONL | `dataset/catalog.jsonl` | ✅ |
298
+ | `TOP_K` | Retrieval candidates count | `8` | ❌ |
299
+ | `NUM_CHOICES` | Tools to recommend | `3` | ❌ |
300
+ | `AGENT_OUTPUT_RETRIES` | Structured output validation retries | `3` | ❌ |
301
+ | `EMBED_CATALOG_ON_START` | Pre-embed catalog on startup when FAISS is empty | `1` | ❌ |
302
+ | `LOGLEVEL_CONSOLE` | Console log level | `WARNING` | ❌ |
303
+ | `LOGLEVEL_FILE` | File log level | `INFO` | ❌ |
304
+ | `FILE_LOG` | Enable file logging | `1` | ❌ |
305
+ | `LOG_DIR` | Log directory | `logs` | ❌ |
306
+ | `LOG_PROMPTS` | Save prompt snapshots | `0` | ❌ |
307
+ | `CONFIG_PATH` | Model config file | `config.yaml` | ✅ |
308
+
309
+ ### GraphDB Catalog Sync (Optional)
310
+
311
+ For automatic catalog syncing from a GraphDB instance:
312
+
313
+ ```dotenv
314
+ GRAPHDB_URL=https://your-graphdb.example.com
315
+ GRAPHDB_GRAPH=your-graph-name
316
+ GRAPHDB_USER=username
317
+ GRAPHDB_PASSWORD=password
318
+ GRAPHDB_QUERY_FILE=/path/to/query.rq
319
+ SYNC_EVERY_HOURS=24 # Auto-refresh interval (0 to disable)
320
+ OUTPUT_JSONLD=dataset/catalog.jsonld
321
+ OUTPUT_JSONL=dataset/catalog.jsonl
322
+ ```
323
+
324
+ Run manual sync:
325
+ ```bash
326
+ ai_agent sync
327
+ ```
328
+
329
+ ---
330
+
331
+ ## 📋 Catalog Format
332
+
333
+ The catalog is a JSONL file where each line is a `SoftwareDoc` following schema.org SoftwareSourceCode structure.
334
+
335
+ ### Minimal Example
336
+
337
+ ```json
338
+ {
339
+ "name": "3d-lungs-segmentation",
340
+ "description": "3D lung segmentation from CT; returns a mask/overlay.",
341
+
342
+ "applicationCategory": ["Medical Imaging"],
343
+ "featureList": ["segmentation"],
344
+ "imagingModality": ["CT"],
345
+ "dims": [3],
346
+ "anatomy": ["lung"],
347
+ "keywords": ["mask", "overlay", "lung segmentation", "CT"],
348
+
349
+ "programmingLanguage": "Python",
350
+ "requiresGPU": false,
351
+ "isAccessibleForFree": true,
352
+ "license": "Apache-2.0",
353
+
354
+ "supportingData": [
355
+ {
356
+ "datasetFormat": "TIFF",
357
+ "bodySite": "lung",
358
+ "imagingModality": "CT",
359
+ "hasDimensionality": 3
360
+ },
361
+ {
362
+ "datasetFormat": "DICOM",
363
+ "bodySite": "lung",
364
+ "imagingModality": "CT",
365
+ "hasDimensionality": 3
366
+ }
367
+ ],
368
+
369
+ "runnableExample": [
370
+ {
371
+ "hostType": "gradio",
372
+ "url": "https://huggingface.co/spaces/qchapp/3d-lungs-segmentation",
373
+ "name": "HF Space"
374
+ }
375
+ ]
376
+ }
377
+ ```
378
+
379
+ ### Key Fields
380
+
381
+ - **name**: Unique identifier for the tool
382
+ - **description**: Clear explanation of what the tool does
383
+ - **featureList**: Operations (e.g., segmentation, registration, classification)
384
+ - **imagingModality**: Medical imaging types (CT, MRI, XR, US, PET)
385
+ - **dims**: Supported dimensions (2D, 3D, 4D)
386
+ - **anatomy**: Body parts/organs
387
+ - **supportingData**: Format compatibility information (critical for matching)
388
+ - **runnableExample**: Links to live demos (HuggingFace Spaces, notebooks, web apps)
389
+
390
+ ---
391
+
392
+ ## 🔧 Development
393
+
394
+ ### Project Structure
395
+
396
+ ```
397
+ ai-agent/
398
+ ├── src/ai_agent/
399
+ │ ├── agent/ # Pydantic AI agent and tools
400
+ │ │ ├── agent.py # Agent definition
401
+ │ │ ├── models.py # Agent state models
402
+ │ │ ├── tools/ # Agent tool implementations
403
+ │ │ │ ├── search_tool.py
404
+ │ │ │ ├── search_alternative_tool.py
405
+ │ │ │ ├── gradio_space_tool.py
406
+ │ │ │ ├── repo_info_tool.py
407
+ │ │ │ └── deepwiki_tool.py
408
+ │ │ └── utils.py
409
+ │ ├── api/ # Pipeline orchestration
410
+ │ │ └── pipeline.py # RAGImagingPipeline
411
+ │ ├── retriever/ # Retrieval components
412
+ │ │ ├── text_embedder.py
413
+ │ │ ├── vector_index.py
414
+ │ │ ├── reranker.py
415
+ │ │ └── software_doc.py
416
+ │ ├── generator/ # Agent prompts and schemas
417
+ │ │ ├── prompts.py
418
+ │ │ └── schema.py
419
+ │ ├── ui/ # Gradio interface
420
+ │ │ ├── app.py
421
+ │ │ ├── handlers.py
422
+ │ │ ├── components.py
423
+ │ │ ├── formatters.py
424
+ │ │ ├── state.py
425
+ │ │ └── visualizations.py
426
+ │ ├── utils/ # Shared utilities
427
+ │ │ ├── config.py # Configuration management
428
+ │ │ ├── file_validator.py
429
+ │ │ ├── image_meta.py # Metadata extraction
430
+ │ │ ├── image_io.py
431
+ │ │ ├── previews.py
432
+ │ │ └── tags.py
433
+ │ ├── catalog/ # Catalog syncing
434
+ │ │ └── sync.py
435
+ │ └── cli.py # CLI entry point
436
+ ├── tests/ # Test suite
437
+ │ ├── test_retrieval_pipeline.py
438
+ │ ├── test_repo_summary.py
439
+ │ └── data/
440
+ ├── artifacts/ # Generated artifacts
441
+ │ └── rag_index/ # FAISS index
442
+ ├── dataset/ # Catalog data
443
+ │ └── catalog.jsonl
444
+ ├── logs/ # Application logs
445
+ ├── config.yaml # Model configuration
446
+ ├── pyproject.toml # Project metadata & dependencies
447
+ ├── Dockerfile # Production Docker image
448
+ ├── tools/image/Dockerfile # Development Docker image
449
+ └── justfile # Task runner commands
450
+ ```
451
+
452
+ ### Local Development
453
+
454
+ ```bash
455
+ # Install in development mode
456
+ pip install -e ".[dev]"
457
+
458
+ # Run tests
459
+ pytest tests/
460
+ ```
461
+
462
+ ### Testing
463
+
464
+ Run the test suite:
465
+
466
+ ```bash
467
+ # All tests
468
+ pytest tests/
469
+
470
+ # Specific test file
471
+ pytest tests/test_retrieval_pipeline.py
472
+
473
+ # With verbose output
474
+ pytest -v tests/
475
+
476
+ # With coverage
477
+ pytest --cov=ai_agent tests/
478
+ ```
479
+
480
+ ### Logging & Debugging
481
+
482
+ **Console Logs**: Set `LOGLEVEL_CONSOLE=DEBUG` for verbose output
483
+
484
+ **File Logs**: Automatically saved to `logs/app_YYYYMMDD.log` (rotates daily)
485
+
486
+ **Prompt Snapshots**: Enable `LOG_PROMPTS=1` to save:
487
+ - `logs/vlm_selector_YYYYMMDD_HHMMSS.txt` - System/user prompts
488
+
489
+ ---
490
+
491
+ ## 📚 API & CLI Reference
492
+
493
+ ### CLI Commands
494
+
495
+ ```bash
496
+ # Launch chat interface
497
+ ai_agent chat
498
+
499
+ # Sync catalog from GraphDB
500
+ ai_agent sync
501
+ ```
502
+
503
+ ## 🗺️ Maintainer Guide
504
+
505
+ For full project documentation with detailed folder responsibilities, environment defaults, and improvement guidelines, see [docs/guide.md](docs/guide.md).
506
+
507
+ ---
508
+
509
+ ## 📝 Changelog
510
+
511
+ See [CHANGELOG.md](CHANGELOG.md) for detailed version history.
512
+
513
+ ### Recent Highlights
514
+
515
+ **[1.0.0]**
516
+ - ✨ New chat-based interface (`ai_agent chat`) with rich media and tool integration
517
+ - 🛠️ Fully agent-based architecture replacing legacy pipelines
518
+ - 🔍 Smarter retrieval with automatic retry
519
+ - 🔗 DeepWiki MCP integration for fast GitHub repository documentation access
520
+ - 🔧 YAML configuration (`config.yaml`) for flexible model and backend setup
521
+ - 🎨 Redesigned UI with Imaging Plaza branding and improved UX
522
+ - ⚡ Performance improvements (pre-embedding, caching, faster startup)
523
+ - 🧹 Major cleanup: removed deprecated code paths, legacy UI, and outdated tests
524
+
525
+ **[0.1.3] - 2025-10-22**
526
+ - Gradio space runner tool
527
+ - Repository info tool
528
+ - UI fixes and polish
529
+
530
+ ---
531
+
532
+ ## 📄 License
533
+
534
+ This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
535
+
536
+ ---
537
+
538
+ ## 🙏 Credits & Acknowledgments
539
+
540
+ **Developed by**: Imaging Plaza Team
541
+
542
+ **Technologies:**
543
+ - [Pydantic AI](https://github.com/pydantic/pydantic-ai) - AI agent framework
544
+ - [OpenAI](https://openai.com) - GPT vision model
545
+ - [FAISS](https://github.com/facebookresearch/faiss) - Vector search
546
+ - [BGE-M3](https://huggingface.co/BAAI/bge-m3) - Multilingual embeddings
547
+ - [Gradio](https://gradio.app) - Interactive web UI
548
+ - [DeepWiki](https://deepwiki.com) - GitHub repository documentation
549
+
550
+ **Medical Imaging Formats:**
551
+ - [pydicom](https://github.com/pydicom/pydicom) - DICOM support
552
+ - [nibabel](https://nipy.org/nibabel/) - NIfTI support
553
+
554
+ ---
555
+
556
+ ## 📮 Support
557
+
558
+ For issues, questions, or contributions, please contact the Imaging Plaza team.
559
+
560
+ ---
561
+
562
+ **🏥 Medical Disclaimer**: This software is a tool recommendation system, not a diagnostic tool. Always consult qualified medical professionals for clinical decisions.
config.yaml CHANGED
@@ -1,64 +1,64 @@
1
- # AI Agent Model Configuration
2
-
3
- # Default/fallback model (used for CLI and initial startup)
4
- agent_model:
5
- # name: "gpt-5.1"
6
- # base_url: null # null for default OpenAI endpoint
7
- # api_key_env: "OPENAI_API_KEY"
8
- name: "openai/gpt-oss-120b"
9
- base_url: "https://inference-rcp.epfl.ch/v1"
10
- api_key_env: "EPFL_API_KEY"
11
-
12
- # Available models for UI dropdown
13
- available_models:
14
- - display_name: "gpt-4o-mini"
15
- name: "gpt-4o-mini"
16
- base_url: null
17
- provider: "OpenAI"
18
- api_key_env: "OPENAI_API_KEY"
19
-
20
- - display_name: "gpt-4o"
21
- name: "gpt-4o"
22
- base_url: null
23
- provider: "OpenAI"
24
- api_key_env: "OPENAI_API_KEY"
25
-
26
- - display_name: "gpt-5-mini"
27
- name: "gpt-5-mini"
28
- base_url: null
29
- provider: "OpenAI"
30
- api_key_env: "OPENAI_API_KEY"
31
-
32
- - display_name: "gpt-5.1"
33
- name: "gpt-5.1"
34
- base_url: null
35
- provider: "OpenAI"
36
- api_key_env: "OPENAI_API_KEY"
37
-
38
- - display_name: "openai/gpt-oss-120b [EPFL]"
39
- name: "openai/gpt-oss-120b"
40
- base_url: "https://inference-rcp.epfl.ch/v1"
41
- provider: "EPFL"
42
- api_key_env: "EPFL_API_KEY"
43
-
44
- # Retrieval stack (embedder + reranker)
45
- retrieval:
46
- embedder:
47
- backend: "remote" # "remote" or "local"
48
- model_name: "Qwen/Qwen3-Embedding-8B"
49
- base_url: "https://inference-rcp.epfl.ch/v1"
50
- api_key_env: "EPFL_API_KEY_EMBEDDER"
51
- timeout_s: 20
52
- # local example:
53
- # backend: "local"
54
- # model_name: "BAAI/bge-m3"
55
-
56
- reranker:
57
- backend: "remote" # "remote" or "local"
58
- model_name: "BAAI/bge-reranker-v2-m3"
59
- base_url: "https://inference-rcp.epfl.ch/v1"
60
- api_key_env: "EPFL_API_KEY_EMBEDDER"
61
- timeout_s: 20
62
- # local example:
63
- # backend: "local"
64
- # model_name: "BAAI/bge-reranker-v2-m3"
 
1
+ # AI Agent Model Configuration
2
+
3
+ # Default/fallback model (used for CLI and initial startup)
4
+ agent_model:
5
+ # name: "gpt-5.1"
6
+ # base_url: null # null for default OpenAI endpoint
7
+ # api_key_env: "OPENAI_API_KEY"
8
+ name: "openai/gpt-oss-120b"
9
+ base_url: "https://inference-rcp.epfl.ch/v1"
10
+ api_key_env: "EPFL_API_KEY"
11
+
12
+ # Available models for UI dropdown
13
+ available_models:
14
+ - display_name: "gpt-4o-mini"
15
+ name: "gpt-4o-mini"
16
+ base_url: null
17
+ provider: "OpenAI"
18
+ api_key_env: "OPENAI_API_KEY"
19
+
20
+ - display_name: "gpt-4o"
21
+ name: "gpt-4o"
22
+ base_url: null
23
+ provider: "OpenAI"
24
+ api_key_env: "OPENAI_API_KEY"
25
+
26
+ - display_name: "gpt-5-mini"
27
+ name: "gpt-5-mini"
28
+ base_url: null
29
+ provider: "OpenAI"
30
+ api_key_env: "OPENAI_API_KEY"
31
+
32
+ - display_name: "gpt-5.1"
33
+ name: "gpt-5.1"
34
+ base_url: null
35
+ provider: "OpenAI"
36
+ api_key_env: "OPENAI_API_KEY"
37
+
38
+ - display_name: "openai/gpt-oss-120b [EPFL]"
39
+ name: "openai/gpt-oss-120b"
40
+ base_url: "https://inference-rcp.epfl.ch/v1"
41
+ provider: "EPFL"
42
+ api_key_env: "EPFL_API_KEY"
43
+
44
+ # Retrieval stack (embedder + reranker)
45
+ retrieval:
46
+ embedder:
47
+ backend: "remote" # "remote" or "local"
48
+ model_name: "Qwen/Qwen3-Embedding-8B"
49
+ base_url: "https://inference-rcp.epfl.ch/v1"
50
+ api_key_env: "EPFL_API_KEY_EMBEDDER"
51
+ timeout_s: 20
52
+ # local example:
53
+ # backend: "local"
54
+ # model_name: "BAAI/bge-m3"
55
+
56
+ reranker:
57
+ backend: "remote" # "remote" or "local"
58
+ model_name: "BAAI/bge-reranker-v2-m3"
59
+ base_url: "https://inference-rcp.epfl.ch/v1"
60
+ api_key_env: "EPFL_API_KEY_EMBEDDER"
61
+ timeout_s: 20
62
+ # local example:
63
+ # backend: "local"
64
+ # model_name: "BAAI/bge-reranker-v2-m3"
data/query.rq ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ CONSTRUCT {{
2
+ ?s ?p ?o
3
+ }}
4
+ WHERE {{
5
+ GRAPH <{graph}> {{
6
+ ?s ?p ?o .
7
+ }}
8
+ }}
docker-compose.yml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ ai-agent:
3
+ build: .
4
+ image: ai-agent:dev
5
+ container_name: ai-agent
6
+ restart: unless-stopped
7
+ env_file: .env
8
+ environment:
9
+ HOST: 0.0.0.0
10
+ PORT: "7860"
11
+ expose:
12
+ - "7860"
13
+ volumes:
14
+ - ./data:/home/user/app/data
15
+ - ./dataset:/home/user/app/dataset
16
+ - ./artifacts:/home/user/app/artifacts
17
+ - ./logs:/home/user/app/logs
18
+
19
+ cloudflared:
20
+ image: cloudflare/cloudflared:latest
21
+ container_name: ai-agent-cloudflared
22
+ restart: unless-stopped
23
+ command: tunnel --no-autoupdate --url http://ai-agent:7860
24
+ depends_on:
25
+ - ai-agent
docs/architecture/agent.md CHANGED
@@ -1,619 +1,619 @@
1
- # Agent & VLM Selection
2
-
3
- The second stage of the pipeline uses a vision-language model (VLM) with PydanticAI agents to select and rank the best tools from candidates.
4
-
5
- ## Overview
6
-
7
- **Goal**: Select the most relevant tools using vision + text understanding
8
-
9
- **Characteristics**:
10
-
11
- - 🧠 Intelligent reasoning with explanations
12
- - 👁️ Vision-aware (analyzes image content)
13
- - 🎯 Comparative ranking of candidates
14
- - 💬 Conversational with context
15
- - 📊 Structured output (Pydantic schemas)
16
-
17
- ## Architecture
18
-
19
- ```mermaid
20
- graph TB
21
- A[User Message + Files] --> B[PydanticAI Agent]
22
- B --> C{Agent Router}
23
- C -->|Tool Call| D[Agent Tools]
24
- C -->|LLM Reasoning| E[GPT-4o/4o-mini]
25
- D --> B
26
- E --> F[ToolSelection Schema]
27
- F --> G[Structured Response]
28
- G --> B
29
- B --> H[Formatted Reply]
30
- ```
31
-
32
- ## PydanticAI Agent
33
-
34
- ### Agent Framework
35
-
36
- **Framework**: [PydanticAI](https://ai.pydantic.dev/)
37
-
38
- **Benefits**:
39
-
40
- - Type-safe with Pydantic models
41
- - Structured output validation
42
- - Built-in tool support
43
- - Async/await support
44
- - Easy testing with dependency injection
45
-
46
- ### Agent Definition
47
-
48
- ```python
49
- from pydantic_ai import Agent
50
- from pydantic_ai.models.openai import OpenAIResponsesModel
51
- from pydantic_ai.providers.openai import OpenAIProvider
52
- from ai_agent.generator.prompts import get_agent_system_prompt
53
- from ai_agent.generator.schema import ToolSelection
54
- from ai_agent.agent.utils import AgentState
55
-
56
- provider = OpenAIProvider(api_key=os.getenv("OPENAI_API_KEY"))
57
- openai_model = OpenAIResponsesModel(model_name="gpt-4o-mini", provider=provider)
58
-
59
- agent = Agent(
60
- model=openai_model,
61
- system_prompt=get_agent_system_prompt(num_choices=3),
62
- deps_type=AgentState,
63
- )
64
- ```
65
-
66
- **Key parameters**:
67
-
68
- - `model`: VLM model to use (configurable via `config.yaml`)
69
- - `system_prompt`: Agent role, scoring rules, and output format (from `generator/prompts.py`)
70
- - `deps_type`: `AgentState` — tracks tool calls, quotas, and session overrides
71
- - `output_type`: `ToolSelection` — passed to `agent.run_sync()` to enforce structured JSON output (not set on the `Agent` constructor)
72
-
73
- ### Conversation State
74
-
75
- ```python
76
- from pydantic import BaseModel, Field
77
- from typing import List, Optional, Dict, Any, Set
78
-
79
- class AgentState(BaseModel):
80
- """Holds incremental tool call logs and runtime overrides."""
81
-
82
- tool_calls: List[Dict[str, Any]] = Field(default_factory=list)
83
- tool_counts: Dict[str, int] = Field(default_factory=dict)
84
- disabled_tools: Set[str] = Field(default_factory=set)
85
- excluded_tools: List[str] = Field(default_factory=list)
86
-
87
- # Runtime overrides (session-only)
88
- override_model: Optional[str] = None
89
- override_base_url: Optional[str] = None
90
- override_top_k: Optional[int] = None
91
- override_num_choices: Optional[int] = None
92
-
93
- image_paths: List[str] = Field(default_factory=list)
94
- original_formats: List[str] = Field(default_factory=list)
95
- ```
96
-
97
- **Passed to every tool call** via dependency injection. Also carries per-tool call counts for quota enforcement.
98
-
99
- ## Agent Tools
100
-
101
- Tools extend agent capabilities beyond chat:
102
-
103
- ### search_alternative
104
-
105
- Request alternative search with different query formulation:
106
-
107
- ```python
108
- @agent.tool(retries=2, prepare=cap_prepare)
109
- @limit_tool_calls("search_alternative", cap=3)
110
- async def search_alternative(
111
- ctx: RunContext[AgentState],
112
- alternative_query: str,
113
- excluded: List[str] | None = None,
114
- top_k: int = 12,
115
- ) -> List[dict]:
116
- """Search for tools using an alternative query formulation."""
117
-
118
- inp = SearchAlternativeInput(
119
- alternative_query=alternative_query,
120
- excluded=excluded or [],
121
- top_k=top_k,
122
- original_formats=ctx.deps.original_formats,
123
- image_paths=ctx.deps.image_paths,
124
- )
125
- out = tool_search_alternative(inp)
126
- return [c.model_dump(mode="python") for c in out.candidates]
127
- ```
128
-
129
- **Usage**:
130
-
131
- - Agent invokes when user asks for alternatives
132
- - Up to 3 calls per conversation
133
- - Formulates semantically different queries
134
-
135
- **Example**:
136
- ```
137
- User: Show me alternatives
138
- Agent: [Calls search_alternative with "pulmonary segmentation CT"]
139
- ```
140
-
141
- ### repo_info
142
-
143
- Fetch GitHub repository details:
144
-
145
- ```python
146
- @agent.tool(retries=2, prepare=cap_prepare)
147
- @limit_tool_calls("repo_info", cap=12)
148
- async def repo_info(ctx: RunContext[AgentState], url: str, tool_name: str | None = None) -> dict:
149
- """Fetch a short summary of a GitHub repository."""
150
-
151
- # Normalize to canonical GitHub URL
152
- norm_url = coerce_github_url_or_none(url)
153
-
154
- # Call tool_repo_summary (tries DeepWiki MCP first, falls back to repocards)
155
- out = await tool_repo_summary(RepoSummaryInput(url=norm_url, tool_name=tool_name))
156
- return out.model_dump(mode="python")
157
- ```
158
-
159
- **Data sources**:
160
-
161
- 1. **DeepWiki MCP**: Pre-indexed, fast, no rate limits
162
- 2. **Repocards**: Direct fetch, fallback for new repos
163
-
164
- **Returns**:
165
-
166
- - Repository description
167
- - Stars, language, topics
168
- - Last update date
169
- - License information
170
-
171
- **Example**:
172
- ```
173
- User: Tell me about TotalSegmentator
174
- Agent: [Calls repo_info("https://github.com/wasserth/TotalSegmentator")]
175
-
176
- TotalSegmentator is an automated multi-organ segmentation tool...
177
- ⭐ 1.2k stars | Python | Apache-2.0 license
178
- Topics: segmentation, medical-imaging, deep-learning
179
- ```
180
-
181
- ### run_example
182
-
183
- Execute Gradio Space demos (optional, experimental):
184
-
185
- ```python
186
- @agent.tool(retries=0, prepare=cap_prepare)
187
- @limit_tool_calls("run_example", cap=1)
188
- async def run_example(
189
- ctx: RunContext[AgentState],
190
- tool_name: str,
191
- endpoint_url: str | None = None,
192
- extra_text: str | None = None,
193
- ) -> dict:
194
- """Run an example / demo for a given tool via its Gradio space."""
195
-
196
- out = tool_run_example(RunExampleInput(
197
- tool_name=tool_name,
198
- endpoint_url=endpoint_url,
199
- extra_text=extra_text,
200
- ))
201
- return out.model_dump(mode="python")
202
- ```
203
-
204
- **Status**: Partially implemented, limited to specific demo formats.
205
-
206
- ## Selection and Ranking
207
-
208
- The PydanticAI agent performs tool selection and ranking directly as part of its LLM reasoning step. There is no separate `VLMToolSelector` class — the agent's system prompt (defined in `generator/prompts.py`) encodes the scoring rules, and the `ToolSelection` Pydantic schema (defined in `generator/schema.py`) enforces structured output.
209
-
210
- ### System Prompt
211
-
212
- The agent system prompt is assembled by `get_agent_system_prompt()` in `generator/prompts.py` and covers:
213
-
214
- - **Image analysis**: Instructions to analyze the attached preview image and reference visual observations in explanations
215
- - **Tool call sequence**: When to call `search_tools`, `search_alternative`, `repo_info`, and `run_example`
216
- - **Scoring rules**: Accuracy (0–100) = Task match (40) + Format compatibility (30) + Features (30)
217
- - **Output format**: Single JSON object matching the `ToolSelection` schema
218
-
219
- ```python
220
- from ai_agent.generator.prompts import get_agent_system_prompt
221
-
222
- # Generates a prompt that instructs the agent to return up to N ranked choices
223
- system_prompt = get_agent_system_prompt(num_choices=3)
224
- ```
225
-
226
- ### Selection Process
227
-
228
- #### Step 1: Tool Calls (Retrieval)
229
-
230
- The agent calls `search_tools` once (and optionally `search_alternative` up to 3 times) to retrieve candidate tools from the vector index:
231
-
232
- ```
233
- Agent → search_tools(query="segment lungs", top_k=12)
234
- ← [TotalSegmentator, MedSAM, nnU-Net, ...]
235
- ```
236
-
237
- #### Step 2: Verification
238
-
239
- For each finalist the agent plans to recommend, it calls `repo_info` to fetch up-to-date GitHub metadata:
240
-
241
- ```
242
- Agent → repo_info(url="https://github.com/wasserth/TotalSegmentator")
243
- ← {stars: 1200, language: "Python", topics: [...], description: "..."}
244
- ```
245
-
246
- #### Step 3: Structured Output
247
-
248
- The agent returns one JSON object (no prose) that is validated against the `ToolSelection` schema:
249
-
250
- ```python
251
- run_result = agent_instance.run_sync(
252
- user_prompt, # text + optional BinaryContent image
253
- deps=deps, # AgentState with image_paths, excluded_tools, etc.
254
- output_type=ToolSelection,
255
- usage_limits=UsageLimits(tool_calls_limit=20),
256
- )
257
- result = run_result.output # ToolSelection instance
258
- ```
259
-
260
- **Multimodal input**:
261
-
262
- - Text: User task + hidden metadata (format hints, image dimensions)
263
- - Image: PNG preview bytes passed as `BinaryContent(data=image_bytes, media_type="image/png")`
264
- - Context: Conversation history prepended to the prompt
265
-
266
- ### Structured Response Schema
267
-
268
- The `ToolSelection` Pydantic model (in `generator/schema.py`) validates the agent output:
269
-
270
- ```python
271
- from ai_agent.generator.schema import (
272
- ToolSelection, ToolChoice, Conversation,
273
- ConversationStatus, NoToolReason
274
- )
275
-
276
- class ToolChoice(BaseModel):
277
- name: str
278
- rank: int
279
- accuracy: float # 0-100
280
- why: str
281
- demo_link: Optional[str] = None
282
-
283
- class Conversation(BaseModel):
284
- status: ConversationStatus
285
- question: Optional[str] = None # required if status=needs_clarification
286
- context: Optional[str] = None # required if status=needs_clarification
287
- options: Optional[List[str]] = None
288
-
289
- class ToolSelection(BaseModel):
290
- conversation: Conversation
291
- choices: List[ToolChoice] = []
292
- explanation: Optional[str] = None
293
- reason: Optional[NoToolReason] = None
294
- ```
295
-
296
- **Example response** (`ToolSelection`):
297
- ```json
298
- {
299
- "conversation": {"status": "complete", "question": null, "context": null},
300
- "choices": [
301
- {
302
- "rank": 1,
303
- "name": "TotalSegmentator",
304
- "accuracy": 95.0,
305
- "why": "Specifically designed for automated multi-organ CT segmentation...",
306
- "demo_link": "https://huggingface.co/spaces/..."
307
- },
308
- {
309
- "rank": 2,
310
- "name": "MedSAM",
311
- "accuracy": 85.0,
312
- "why": "Flexible SAM-based segmentation supporting DICOM input...",
313
- "demo_link": "https://huggingface.co/spaces/..."
314
- }
315
- ],
316
- "explanation": null,
317
- "reason": null
318
- }
319
- ```
320
-
321
- ### Validation
322
-
323
- Pydantic validates:
324
-
325
- - All required fields present
326
- - Types correct (`int`, `float`, `str`, enum)
327
- - `accuracy` within 0–100 range
328
- - `ConversationStatus` is one of the allowed enum values
329
- - `NoToolReason` is a valid enum value when `choices` is empty
330
-
331
- `ToolSelection.normalize()` also enforces consistency rules automatically (e.g. setting `status=complete` when choices are returned, `status=needs_clarification` when a question is present).
332
-
333
- ## Conversation States
334
-
335
- State machine for conversation flow:
336
-
337
- ```python
338
- from ai_agent.generator.schema import ConversationStatus
339
-
340
- class ConversationStatus(str, Enum):
341
- COMPLETE = "complete" # Recommendations provided (or no tool found)
342
- NEEDS_CLARIFICATION = "needs_clarification" # Agent needs more info
343
- ```
344
-
345
- ### Complete
346
-
347
- Normal successful response:
348
-
349
- ```python
350
- {
351
- "conversation": {"status": "complete", "question": null, "context": null},
352
- "choices": [...],
353
- "explanation": null,
354
- "reason": null
355
- }
356
- ```
357
-
358
- **Triggers**:
359
-
360
- - Query is clear
361
- - Candidates found
362
- - Image/metadata sufficient
363
-
364
- ### Needs Clarification
365
-
366
- Agent requests more information:
367
-
368
- ```python
369
- {
370
- "conversation": {
371
- "status": "needs_clarification",
372
- "question": "Which specific organ would you like to segment?",
373
- "context": "Several segmentation tools available; target organ narrows choices.",
374
- "options": ["Lungs", "Brain", "Liver", "Other (briefly specify)"]
375
- },
376
- "choices": [],
377
- "explanation": null,
378
- "reason": null
379
- }
380
- ```
381
-
382
- **Triggers**:
383
- - Ambiguous query
384
- - Multiple valid interpretations
385
- - Missing critical information
386
-
387
- **Example flow**:
388
- ```
389
- User: Segment this MRI
390
- Agent: [STATUS: needs_clarification] Which organ would you like to segment?
391
- User: The brain
392
- Agent: [STATUS: complete] Here are brain segmentation tools...
393
- ```
394
-
395
- ### No Tool Terminal
396
-
397
- No suitable tools in catalog — `status` is still `complete`, but `choices` is empty and a `reason` + `explanation` are provided:
398
-
399
- ```python
400
- {
401
- "conversation": {"status": "complete", "question": null, "context": null},
402
- "choices": [],
403
- "reason": "no_task_match",
404
- "explanation": "No tools in the catalog handle audio processing. This catalog covers imaging analysis software."
405
- }
406
- ```
407
-
408
- Available `NoToolReason` values: `no_suitable_tool`, `no_modality_match`, `no_task_match`, `no_dimension_match`, `invalid_files`.
409
-
410
- ## Ranking Logic
411
-
412
- ### Scoring Factors
413
-
414
- The agent considers:
415
-
416
- #### High Priority
417
- 1. **Task Match**: Tool designed for this specific task
418
- 2. **Format Compatibility**: Supports user's file format
419
- 3. **Visual Analysis**: Image content matches tool's domain
420
-
421
- #### Medium Priority
422
- 4. **Modality Alignment**: CT tool for CT image, MRI for MRI
423
- 5. **Dimension Match**: 3D tool for 3D volume
424
- 6. **Feature Coverage**: Specific capabilities mentioned
425
-
426
- #### Low Priority
427
- 7. **License**: Open-source preference (if no preference stated)
428
- 8. **Demo Availability**: Has runnable demo
429
- 9. **Popularity**: Community adoption
430
-
431
- ### Explanation Generation
432
-
433
- Each recommendation includes explanation:
434
-
435
- **Good explanation template**:
436
- ```
437
- {Tool} is {specifically designed / well-suited} for {task}
438
- on {modality} images. It supports {format} input {with/without}
439
- preprocessing and provides {key features}. {Caveats if any}.
440
- ```
441
-
442
- **Example**:
443
- ```
444
- TotalSegmentator is specifically designed for automated multi-organ
445
- segmentation on CT scans. It supports DICOM input without preprocessing
446
- and can segment 104 anatomical structures including lungs, air airways,
447
- and vessels. It works best on whole-body CT but also performs well on
448
- thoracic scans.
449
- ```
450
-
451
- ### Rank Assignment
452
-
453
- - **Rank 1**: Best overall match (highest accuracy score)
454
- - **Rank 2**: Strong alternative or different approach
455
- - **Rank 3**: Fallback option or specialized capability
456
-
457
- **Important**: Ranks are relative to **this specific query**, not absolute tool quality.
458
-
459
- ## Model Configuration
460
-
461
- ### Model Selection
462
-
463
- Available via `config.yaml`:
464
-
465
- ```yaml
466
- agent_model:
467
- name: "gpt-4o-mini"
468
- base_url: null
469
- api_key_env: "OPENAI_API_KEY"
470
- ```
471
-
472
- ### Model Comparison
473
-
474
- | Model | Vision | Speed | Cost | Best For |
475
- |-------|--------|-------|------|----------|
476
- | gpt-4o-mini | ✅ | ⚡⚡⚡ | $ | Most queries, fast iteration |
477
- | gpt-4o | ✅✅ | ⚡⚡ | $$ | Complex visual analysis |
478
- | gpt-5.1 | ✅✅✅ | ⚡ | $$$ | Maximum accuracy needed |
479
-
480
- ### Custom Endpoints
481
-
482
- Support for OpenAI-compatible APIs:
483
-
484
- ```yaml
485
- agent_model:
486
- name: "llama-3.2-vision"
487
- base_url: "https://inference.epfl.ch/v1"
488
- api_key_env: "EPFL_API_KEY"
489
- ```
490
-
491
- ## Error Handling
492
-
493
- ### Agent Errors
494
-
495
- **Tool quota exceeded** (handled gracefully in `run_agent`):
496
- ```python
497
- except UsageLimitExceeded:
498
- # Returns a ToolSelection with empty choices and an explanation
499
- result = ToolSelection(
500
- conversation=Conversation(status=ConversationStatus.COMPLETE, ...),
501
- choices=[],
502
- explanation="Tool call limit reached. Try a more specific query.",
503
- )
504
- ```
505
-
506
- **Invalid structured output**:
507
-
508
- PydanticAI automatically retries the LLM call (up to `retries=2` per tool) if the model returns output that fails `ToolSelection` validation. The `ToolSelection.normalize()` model validator also auto-corrects minor inconsistencies.
509
-
510
- **API Errors**:
511
- ```python
512
- except Exception as e:
513
- log.warning(f"Agent execution encountered an error: {e}")
514
- raise # propagated to the UI layer
515
- ```
516
-
517
- ### Graceful Degradation
518
-
519
- If the agent fails after all retries:
520
-
521
- 1. Return empty `choices` with an `explanation` describing what was searched
522
- 2. UI surfaces the explanation so users can refine their query
523
- 3. Suggest manual exploration of the catalog
524
-
525
- <!-- ## Performance
526
-
527
- ### Latency
528
-
529
- Typical VLM call: **2-5 seconds**
530
-
531
- Breakdown:
532
-
533
- - Prompt construction: <100ms
534
- - API call: 2-4s (network + inference)
535
- - Response parsing: <100ms
536
- - Validation: <50ms
537
-
538
- ### Optimization
539
-
540
- **Prompt optimization**:
541
-
542
- - Concise candidate descriptions
543
- - Limit to top-8 candidates
544
- - Structured format for parsing
545
-
546
- **Caching**:
547
-
548
- - Model endpoint reused
549
- - Agent instance persists across requests
550
-
551
- **Batch processing** (for testing):
552
- ```python
553
- # Process multiple queries
554
- responses = await asyncio.gather(*[
555
- agent.run(query1),
556
- agent.run(query2),
557
- agent.run(query3)
558
- ])
559
- ``` -->
560
-
561
- ## Testing
562
-
563
- ### Unit Tests
564
-
565
- Test agent selection with PydanticAI's built-in test model (your catalog should contain the choice provided below, i.e. the `TotalSegmentator` tool):
566
-
567
- ```python
568
- from pydantic_ai import Agent
569
- from pydantic_ai.models.test import TestModel
570
- from ai_agent.generator.schema import ToolSelection, Conversation, ConversationStatus, ToolChoice
571
- from ai_agent.agent.utils import AgentState
572
-
573
- def test_agent_selection():
574
- test_model = TestModel()
575
- test_agent = Agent(model=test_model, deps_type=AgentState)
576
-
577
- mock_output = ToolSelection(
578
- conversation=Conversation(status=ConversationStatus.COMPLETE),
579
- choices=[
580
- ToolChoice(name="TotalSegmentator", rank=1, accuracy=95.0, why="Best CT segmenter")
581
- ]
582
- )
583
-
584
- with test_agent.override(model=test_model):
585
- result = test_agent.run_sync("segment lungs", deps=AgentState(), output_type=ToolSelection)
586
-
587
- assert result.output.conversation.status == ConversationStatus.COMPLETE
588
- assert len(result.output.choices) == 1
589
- assert result.output.choices[0].rank == 1
590
- ```
591
-
592
- ### Integration Tests
593
-
594
- Test with real VLM (expensive, slow):
595
-
596
- ```python
597
- @pytest.mark.integration
598
- def test_real_agent():
599
- from ai_agent.agent.agent import run_agent
600
-
601
- with open("tests/data/sample.tif", "rb") as f:
602
- image_bytes = f.read()
603
-
604
- result = run_agent(
605
- task="I want to segment the lungs of this CT scan",
606
- image_paths=["tests/data/sample.tif"],
607
- image_bytes=image_bytes,
608
- )
609
-
610
- assert result.conversation.status == ConversationStatus.COMPLETE
611
- assert len(result.choices) > 0
612
- assert all(0 <= c.accuracy <= 100 for c in result.choices)
613
- ```
614
-
615
- ## Next Steps
616
-
617
- - Learn about [Software Catalog](catalog.md)
618
- - Return to [Architecture Overview](overview.md)
619
- - Explore [Retrieval Pipeline](retrieval.md)
 
1
+ # Agent & VLM Selection
2
+
3
+ The second stage of the pipeline uses a vision-language model (VLM) with PydanticAI agents to select and rank the best tools from candidates.
4
+
5
+ ## Overview
6
+
7
+ **Goal**: Select the most relevant tools using vision + text understanding
8
+
9
+ **Characteristics**:
10
+
11
+ - 🧠 Intelligent reasoning with explanations
12
+ - 👁️ Vision-aware (analyzes image content)
13
+ - 🎯 Comparative ranking of candidates
14
+ - 💬 Conversational with context
15
+ - 📊 Structured output (Pydantic schemas)
16
+
17
+ ## Architecture
18
+
19
+ ```mermaid
20
+ graph TB
21
+ A[User Message + Files] --> B[PydanticAI Agent]
22
+ B --> C{Agent Router}
23
+ C -->|Tool Call| D[Agent Tools]
24
+ C -->|LLM Reasoning| E[GPT-4o/4o-mini]
25
+ D --> B
26
+ E --> F[ToolSelection Schema]
27
+ F --> G[Structured Response]
28
+ G --> B
29
+ B --> H[Formatted Reply]
30
+ ```
31
+
32
+ ## PydanticAI Agent
33
+
34
+ ### Agent Framework
35
+
36
+ **Framework**: [PydanticAI](https://ai.pydantic.dev/)
37
+
38
+ **Benefits**:
39
+
40
+ - Type-safe with Pydantic models
41
+ - Structured output validation
42
+ - Built-in tool support
43
+ - Async/await support
44
+ - Easy testing with dependency injection
45
+
46
+ ### Agent Definition
47
+
48
+ ```python
49
+ from pydantic_ai import Agent
50
+ from pydantic_ai.models.openai import OpenAIResponsesModel
51
+ from pydantic_ai.providers.openai import OpenAIProvider
52
+ from ai_agent.generator.prompts import get_agent_system_prompt
53
+ from ai_agent.generator.schema import ToolSelection
54
+ from ai_agent.agent.utils import AgentState
55
+
56
+ provider = OpenAIProvider(api_key=os.getenv("OPENAI_API_KEY"))
57
+ openai_model = OpenAIResponsesModel(model_name="gpt-4o-mini", provider=provider)
58
+
59
+ agent = Agent(
60
+ model=openai_model,
61
+ system_prompt=get_agent_system_prompt(num_choices=3),
62
+ deps_type=AgentState,
63
+ )
64
+ ```
65
+
66
+ **Key parameters**:
67
+
68
+ - `model`: VLM model to use (configurable via `config.yaml`)
69
+ - `system_prompt`: Agent role, scoring rules, and output format (from `generator/prompts.py`)
70
+ - `deps_type`: `AgentState` — tracks tool calls, quotas, and session overrides
71
+ - `output_type`: `ToolSelection` — passed to `agent.run_sync()` to enforce structured JSON output (not set on the `Agent` constructor)
72
+
73
+ ### Conversation State
74
+
75
+ ```python
76
+ from pydantic import BaseModel, Field
77
+ from typing import List, Optional, Dict, Any, Set
78
+
79
+ class AgentState(BaseModel):
80
+ """Holds incremental tool call logs and runtime overrides."""
81
+
82
+ tool_calls: List[Dict[str, Any]] = Field(default_factory=list)
83
+ tool_counts: Dict[str, int] = Field(default_factory=dict)
84
+ disabled_tools: Set[str] = Field(default_factory=set)
85
+ excluded_tools: List[str] = Field(default_factory=list)
86
+
87
+ # Runtime overrides (session-only)
88
+ override_model: Optional[str] = None
89
+ override_base_url: Optional[str] = None
90
+ override_top_k: Optional[int] = None
91
+ override_num_choices: Optional[int] = None
92
+
93
+ image_paths: List[str] = Field(default_factory=list)
94
+ original_formats: List[str] = Field(default_factory=list)
95
+ ```
96
+
97
+ **Passed to every tool call** via dependency injection. Also carries per-tool call counts for quota enforcement.
98
+
99
+ ## Agent Tools
100
+
101
+ Tools extend agent capabilities beyond chat:
102
+
103
+ ### search_alternative
104
+
105
+ Request alternative search with different query formulation:
106
+
107
+ ```python
108
+ @agent.tool(retries=2, prepare=cap_prepare)
109
+ @limit_tool_calls("search_alternative", cap=3)
110
+ async def search_alternative(
111
+ ctx: RunContext[AgentState],
112
+ alternative_query: str,
113
+ excluded: List[str] | None = None,
114
+ top_k: int = 12,
115
+ ) -> List[dict]:
116
+ """Search for tools using an alternative query formulation."""
117
+
118
+ inp = SearchAlternativeInput(
119
+ alternative_query=alternative_query,
120
+ excluded=excluded or [],
121
+ top_k=top_k,
122
+ original_formats=ctx.deps.original_formats,
123
+ image_paths=ctx.deps.image_paths,
124
+ )
125
+ out = tool_search_alternative(inp)
126
+ return [c.model_dump(mode="python") for c in out.candidates]
127
+ ```
128
+
129
+ **Usage**:
130
+
131
+ - Agent invokes when user asks for alternatives
132
+ - Up to 3 calls per conversation
133
+ - Formulates semantically different queries
134
+
135
+ **Example**:
136
+ ```
137
+ User: Show me alternatives
138
+ Agent: [Calls search_alternative with "pulmonary segmentation CT"]
139
+ ```
140
+
141
+ ### repo_info
142
+
143
+ Fetch GitHub repository details:
144
+
145
+ ```python
146
+ @agent.tool(retries=2, prepare=cap_prepare)
147
+ @limit_tool_calls("repo_info", cap=12)
148
+ async def repo_info(ctx: RunContext[AgentState], url: str, tool_name: str | None = None) -> dict:
149
+ """Fetch a short summary of a GitHub repository."""
150
+
151
+ # Normalize to canonical GitHub URL
152
+ norm_url = coerce_github_url_or_none(url)
153
+
154
+ # Call tool_repo_summary (tries DeepWiki MCP first, falls back to repocards)
155
+ out = await tool_repo_summary(RepoSummaryInput(url=norm_url, tool_name=tool_name))
156
+ return out.model_dump(mode="python")
157
+ ```
158
+
159
+ **Data sources**:
160
+
161
+ 1. **DeepWiki MCP**: Pre-indexed, fast, no rate limits
162
+ 2. **Repocards**: Direct fetch, fallback for new repos
163
+
164
+ **Returns**:
165
+
166
+ - Repository description
167
+ - Stars, language, topics
168
+ - Last update date
169
+ - License information
170
+
171
+ **Example**:
172
+ ```
173
+ User: Tell me about TotalSegmentator
174
+ Agent: [Calls repo_info("https://github.com/wasserth/TotalSegmentator")]
175
+
176
+ TotalSegmentator is an automated multi-organ segmentation tool...
177
+ ⭐ 1.2k stars | Python | Apache-2.0 license
178
+ Topics: segmentation, medical-imaging, deep-learning
179
+ ```
180
+
181
+ ### run_example
182
+
183
+ Execute Gradio Space demos (optional, experimental):
184
+
185
+ ```python
186
+ @agent.tool(retries=0, prepare=cap_prepare)
187
+ @limit_tool_calls("run_example", cap=1)
188
+ async def run_example(
189
+ ctx: RunContext[AgentState],
190
+ tool_name: str,
191
+ endpoint_url: str | None = None,
192
+ extra_text: str | None = None,
193
+ ) -> dict:
194
+ """Run an example / demo for a given tool via its Gradio space."""
195
+
196
+ out = tool_run_example(RunExampleInput(
197
+ tool_name=tool_name,
198
+ endpoint_url=endpoint_url,
199
+ extra_text=extra_text,
200
+ ))
201
+ return out.model_dump(mode="python")
202
+ ```
203
+
204
+ **Status**: Partially implemented, limited to specific demo formats.
205
+
206
+ ## Selection and Ranking
207
+
208
+ The PydanticAI agent performs tool selection and ranking directly as part of its LLM reasoning step. There is no separate `VLMToolSelector` class — the agent's system prompt (defined in `generator/prompts.py`) encodes the scoring rules, and the `ToolSelection` Pydantic schema (defined in `generator/schema.py`) enforces structured output.
209
+
210
+ ### System Prompt
211
+
212
+ The agent system prompt is assembled by `get_agent_system_prompt()` in `generator/prompts.py` and covers:
213
+
214
+ - **Image analysis**: Instructions to analyze the attached preview image and reference visual observations in explanations
215
+ - **Tool call sequence**: When to call `search_tools`, `search_alternative`, `repo_info`, and `run_example`
216
+ - **Scoring rules**: Accuracy (0–100) = Task match (40) + Format compatibility (30) + Features (30)
217
+ - **Output format**: Single JSON object matching the `ToolSelection` schema
218
+
219
+ ```python
220
+ from ai_agent.generator.prompts import get_agent_system_prompt
221
+
222
+ # Generates a prompt that instructs the agent to return up to N ranked choices
223
+ system_prompt = get_agent_system_prompt(num_choices=3)
224
+ ```
225
+
226
+ ### Selection Process
227
+
228
+ #### Step 1: Tool Calls (Retrieval)
229
+
230
+ The agent calls `search_tools` once (and optionally `search_alternative` up to 3 times) to retrieve candidate tools from the vector index:
231
+
232
+ ```
233
+ Agent → search_tools(query="segment lungs", top_k=12)
234
+ ← [TotalSegmentator, MedSAM, nnU-Net, ...]
235
+ ```
236
+
237
+ #### Step 2: Verification
238
+
239
+ For each finalist the agent plans to recommend, it calls `repo_info` to fetch up-to-date GitHub metadata:
240
+
241
+ ```
242
+ Agent → repo_info(url="https://github.com/wasserth/TotalSegmentator")
243
+ ← {stars: 1200, language: "Python", topics: [...], description: "..."}
244
+ ```
245
+
246
+ #### Step 3: Structured Output
247
+
248
+ The agent returns one JSON object (no prose) that is validated against the `ToolSelection` schema:
249
+
250
+ ```python
251
+ run_result = agent_instance.run_sync(
252
+ user_prompt, # text + optional BinaryContent image
253
+ deps=deps, # AgentState with image_paths, excluded_tools, etc.
254
+ output_type=ToolSelection,
255
+ usage_limits=UsageLimits(tool_calls_limit=20),
256
+ )
257
+ result = run_result.output # ToolSelection instance
258
+ ```
259
+
260
+ **Multimodal input**:
261
+
262
+ - Text: User task + hidden metadata (format hints, image dimensions)
263
+ - Image: PNG preview bytes passed as `BinaryContent(data=image_bytes, media_type="image/png")`
264
+ - Context: Conversation history prepended to the prompt
265
+
266
+ ### Structured Response Schema
267
+
268
+ The `ToolSelection` Pydantic model (in `generator/schema.py`) validates the agent output:
269
+
270
+ ```python
271
+ from ai_agent.generator.schema import (
272
+ ToolSelection, ToolChoice, Conversation,
273
+ ConversationStatus, NoToolReason
274
+ )
275
+
276
+ class ToolChoice(BaseModel):
277
+ name: str
278
+ rank: int
279
+ accuracy: float # 0-100
280
+ why: str
281
+ demo_link: Optional[str] = None
282
+
283
+ class Conversation(BaseModel):
284
+ status: ConversationStatus
285
+ question: Optional[str] = None # required if status=needs_clarification
286
+ context: Optional[str] = None # required if status=needs_clarification
287
+ options: Optional[List[str]] = None
288
+
289
+ class ToolSelection(BaseModel):
290
+ conversation: Conversation
291
+ choices: List[ToolChoice] = []
292
+ explanation: Optional[str] = None
293
+ reason: Optional[NoToolReason] = None
294
+ ```
295
+
296
+ **Example response** (`ToolSelection`):
297
+ ```json
298
+ {
299
+ "conversation": {"status": "complete", "question": null, "context": null},
300
+ "choices": [
301
+ {
302
+ "rank": 1,
303
+ "name": "TotalSegmentator",
304
+ "accuracy": 95.0,
305
+ "why": "Specifically designed for automated multi-organ CT segmentation...",
306
+ "demo_link": "https://huggingface.co/spaces/..."
307
+ },
308
+ {
309
+ "rank": 2,
310
+ "name": "MedSAM",
311
+ "accuracy": 85.0,
312
+ "why": "Flexible SAM-based segmentation supporting DICOM input...",
313
+ "demo_link": "https://huggingface.co/spaces/..."
314
+ }
315
+ ],
316
+ "explanation": null,
317
+ "reason": null
318
+ }
319
+ ```
320
+
321
+ ### Validation
322
+
323
+ Pydantic validates:
324
+
325
+ - All required fields present
326
+ - Types correct (`int`, `float`, `str`, enum)
327
+ - `accuracy` within 0–100 range
328
+ - `ConversationStatus` is one of the allowed enum values
329
+ - `NoToolReason` is a valid enum value when `choices` is empty
330
+
331
+ `ToolSelection.normalize()` also enforces consistency rules automatically (e.g. setting `status=complete` when choices are returned, `status=needs_clarification` when a question is present).
332
+
333
+ ## Conversation States
334
+
335
+ State machine for conversation flow:
336
+
337
+ ```python
338
+ from ai_agent.generator.schema import ConversationStatus
339
+
340
+ class ConversationStatus(str, Enum):
341
+ COMPLETE = "complete" # Recommendations provided (or no tool found)
342
+ NEEDS_CLARIFICATION = "needs_clarification" # Agent needs more info
343
+ ```
344
+
345
+ ### Complete
346
+
347
+ Normal successful response:
348
+
349
+ ```python
350
+ {
351
+ "conversation": {"status": "complete", "question": null, "context": null},
352
+ "choices": [...],
353
+ "explanation": null,
354
+ "reason": null
355
+ }
356
+ ```
357
+
358
+ **Triggers**:
359
+
360
+ - Query is clear
361
+ - Candidates found
362
+ - Image/metadata sufficient
363
+
364
+ ### Needs Clarification
365
+
366
+ Agent requests more information:
367
+
368
+ ```python
369
+ {
370
+ "conversation": {
371
+ "status": "needs_clarification",
372
+ "question": "Which specific organ would you like to segment?",
373
+ "context": "Several segmentation tools available; target organ narrows choices.",
374
+ "options": ["Lungs", "Brain", "Liver", "Other (briefly specify)"]
375
+ },
376
+ "choices": [],
377
+ "explanation": null,
378
+ "reason": null
379
+ }
380
+ ```
381
+
382
+ **Triggers**:
383
+ - Ambiguous query
384
+ - Multiple valid interpretations
385
+ - Missing critical information
386
+
387
+ **Example flow**:
388
+ ```
389
+ User: Segment this MRI
390
+ Agent: [STATUS: needs_clarification] Which organ would you like to segment?
391
+ User: The brain
392
+ Agent: [STATUS: complete] Here are brain segmentation tools...
393
+ ```
394
+
395
+ ### No Tool Terminal
396
+
397
+ No suitable tools in catalog — `status` is still `complete`, but `choices` is empty and a `reason` + `explanation` are provided:
398
+
399
+ ```python
400
+ {
401
+ "conversation": {"status": "complete", "question": null, "context": null},
402
+ "choices": [],
403
+ "reason": "no_task_match",
404
+ "explanation": "No tools in the catalog handle audio processing. This catalog covers imaging analysis software."
405
+ }
406
+ ```
407
+
408
+ Available `NoToolReason` values: `no_suitable_tool`, `no_modality_match`, `no_task_match`, `no_dimension_match`, `invalid_files`.
409
+
410
+ ## Ranking Logic
411
+
412
+ ### Scoring Factors
413
+
414
+ The agent considers:
415
+
416
+ #### High Priority
417
+ 1. **Task Match**: Tool designed for this specific task
418
+ 2. **Format Compatibility**: Supports user's file format
419
+ 3. **Visual Analysis**: Image content matches tool's domain
420
+
421
+ #### Medium Priority
422
+ 4. **Modality Alignment**: CT tool for CT image, MRI for MRI
423
+ 5. **Dimension Match**: 3D tool for 3D volume
424
+ 6. **Feature Coverage**: Specific capabilities mentioned
425
+
426
+ #### Low Priority
427
+ 7. **License**: Open-source preference (if no preference stated)
428
+ 8. **Demo Availability**: Has runnable demo
429
+ 9. **Popularity**: Community adoption
430
+
431
+ ### Explanation Generation
432
+
433
+ Each recommendation includes explanation:
434
+
435
+ **Good explanation template**:
436
+ ```
437
+ {Tool} is {specifically designed / well-suited} for {task}
438
+ on {modality} images. It supports {format} input {with/without}
439
+ preprocessing and provides {key features}. {Caveats if any}.
440
+ ```
441
+
442
+ **Example**:
443
+ ```
444
+ TotalSegmentator is specifically designed for automated multi-organ
445
+ segmentation on CT scans. It supports DICOM input without preprocessing
446
+ and can segment 104 anatomical structures including lungs, air airways,
447
+ and vessels. It works best on whole-body CT but also performs well on
448
+ thoracic scans.
449
+ ```
450
+
451
+ ### Rank Assignment
452
+
453
+ - **Rank 1**: Best overall match (highest accuracy score)
454
+ - **Rank 2**: Strong alternative or different approach
455
+ - **Rank 3**: Fallback option or specialized capability
456
+
457
+ **Important**: Ranks are relative to **this specific query**, not absolute tool quality.
458
+
459
+ ## Model Configuration
460
+
461
+ ### Model Selection
462
+
463
+ Available via `config.yaml`:
464
+
465
+ ```yaml
466
+ agent_model:
467
+ name: "gpt-4o-mini"
468
+ base_url: null
469
+ api_key_env: "OPENAI_API_KEY"
470
+ ```
471
+
472
+ ### Model Comparison
473
+
474
+ | Model | Vision | Speed | Cost | Best For |
475
+ |-------|--------|-------|------|----------|
476
+ | gpt-4o-mini | ✅ | ⚡⚡⚡ | $ | Most queries, fast iteration |
477
+ | gpt-4o | ✅✅ | ⚡⚡ | $$ | Complex visual analysis |
478
+ | gpt-5.1 | ✅✅✅ | ⚡ | $$$ | Maximum accuracy needed |
479
+
480
+ ### Custom Endpoints
481
+
482
+ Support for OpenAI-compatible APIs:
483
+
484
+ ```yaml
485
+ agent_model:
486
+ name: "llama-3.2-vision"
487
+ base_url: "https://inference.epfl.ch/v1"
488
+ api_key_env: "EPFL_API_KEY"
489
+ ```
490
+
491
+ ## Error Handling
492
+
493
+ ### Agent Errors
494
+
495
+ **Tool quota exceeded** (handled gracefully in `run_agent`):
496
+ ```python
497
+ except UsageLimitExceeded:
498
+ # Returns a ToolSelection with empty choices and an explanation
499
+ result = ToolSelection(
500
+ conversation=Conversation(status=ConversationStatus.COMPLETE, ...),
501
+ choices=[],
502
+ explanation="Tool call limit reached. Try a more specific query.",
503
+ )
504
+ ```
505
+
506
+ **Invalid structured output**:
507
+
508
+ PydanticAI automatically retries the LLM call (up to `retries=2` per tool) if the model returns output that fails `ToolSelection` validation. The `ToolSelection.normalize()` model validator also auto-corrects minor inconsistencies.
509
+
510
+ **API Errors**:
511
+ ```python
512
+ except Exception as e:
513
+ log.warning(f"Agent execution encountered an error: {e}")
514
+ raise # propagated to the UI layer
515
+ ```
516
+
517
+ ### Graceful Degradation
518
+
519
+ If the agent fails after all retries:
520
+
521
+ 1. Return empty `choices` with an `explanation` describing what was searched
522
+ 2. UI surfaces the explanation so users can refine their query
523
+ 3. Suggest manual exploration of the catalog
524
+
525
+ <!-- ## Performance
526
+
527
+ ### Latency
528
+
529
+ Typical VLM call: **2-5 seconds**
530
+
531
+ Breakdown:
532
+
533
+ - Prompt construction: <100ms
534
+ - API call: 2-4s (network + inference)
535
+ - Response parsing: <100ms
536
+ - Validation: <50ms
537
+
538
+ ### Optimization
539
+
540
+ **Prompt optimization**:
541
+
542
+ - Concise candidate descriptions
543
+ - Limit to top-8 candidates
544
+ - Structured format for parsing
545
+
546
+ **Caching**:
547
+
548
+ - Model endpoint reused
549
+ - Agent instance persists across requests
550
+
551
+ **Batch processing** (for testing):
552
+ ```python
553
+ # Process multiple queries
554
+ responses = await asyncio.gather(*[
555
+ agent.run(query1),
556
+ agent.run(query2),
557
+ agent.run(query3)
558
+ ])
559
+ ``` -->
560
+
561
+ ## Testing
562
+
563
+ ### Unit Tests
564
+
565
+ Test agent selection with PydanticAI's built-in test model (your catalog should contain the choice provided below, i.e. the `TotalSegmentator` tool):
566
+
567
+ ```python
568
+ from pydantic_ai import Agent
569
+ from pydantic_ai.models.test import TestModel
570
+ from ai_agent.generator.schema import ToolSelection, Conversation, ConversationStatus, ToolChoice
571
+ from ai_agent.agent.utils import AgentState
572
+
573
+ def test_agent_selection():
574
+ test_model = TestModel()
575
+ test_agent = Agent(model=test_model, deps_type=AgentState)
576
+
577
+ mock_output = ToolSelection(
578
+ conversation=Conversation(status=ConversationStatus.COMPLETE),
579
+ choices=[
580
+ ToolChoice(name="TotalSegmentator", rank=1, accuracy=95.0, why="Best CT segmenter")
581
+ ]
582
+ )
583
+
584
+ with test_agent.override(model=test_model):
585
+ result = test_agent.run_sync("segment lungs", deps=AgentState(), output_type=ToolSelection)
586
+
587
+ assert result.output.conversation.status == ConversationStatus.COMPLETE
588
+ assert len(result.output.choices) == 1
589
+ assert result.output.choices[0].rank == 1
590
+ ```
591
+
592
+ ### Integration Tests
593
+
594
+ Test with real VLM (expensive, slow):
595
+
596
+ ```python
597
+ @pytest.mark.integration
598
+ def test_real_agent():
599
+ from ai_agent.agent.agent import run_agent
600
+
601
+ with open("tests/data/sample.tif", "rb") as f:
602
+ image_bytes = f.read()
603
+
604
+ result = run_agent(
605
+ task="I want to segment the lungs of this CT scan",
606
+ image_paths=["tests/data/sample.tif"],
607
+ image_bytes=image_bytes,
608
+ )
609
+
610
+ assert result.conversation.status == ConversationStatus.COMPLETE
611
+ assert len(result.choices) > 0
612
+ assert all(0 <= c.accuracy <= 100 for c in result.choices)
613
+ ```
614
+
615
+ ## Next Steps
616
+
617
+ - Learn about [Software Catalog](catalog.md)
618
+ - Return to [Architecture Overview](overview.md)
619
+ - Explore [Retrieval Pipeline](retrieval.md)
docs/architecture/catalog.md CHANGED
@@ -1,455 +1,455 @@
1
- # Software Catalog
2
-
3
- The software catalog is the foundation of the AI Imaging Agent, containing curated information about imaging analysis tools.
4
-
5
- ## Overview
6
-
7
- **Format**: JSON Lines (JSONL)
8
- **Location**: `dataset/catalog.jsonl`
9
- **Schema**: Based on schema.org SoftwareSourceCode
10
- **Size**: ~150 tools currently
11
-
12
- ## Catalog Schema
13
-
14
- ### Core Fields
15
-
16
- Based on [schema.org/SoftwareSourceCode](https://schema.org/SoftwareSourceCode):
17
-
18
- ```json
19
- {
20
- "@type": "SoftwareSourceCode",
21
- "name": "TotalSegmentator",
22
- "description": "Tool for automated segmentation of 104 anatomical structures",
23
- "url": "https://github.com/wasserth/TotalSegmentator",
24
- "codeRepository": "https://github.com/wasserth/TotalSegmentator",
25
- "programmingLanguage": "Python",
26
- "runtimePlatform": "PyTorch",
27
- "license": "Apache-2.0",
28
- "keywords": ["segmentation", "CT", "MRI", "medical-imaging"],
29
- "applicationCategory": "Medical Imaging",
30
- "operatingSystem": ["Linux", "Windows", "macOS"],
31
- "softwareVersion": "2.0.0",
32
- "datePublished": "2022-09-01",
33
- "dateModified": "2024-01-15",
34
- "author": {
35
- "@type": "Person",
36
- "name": "Jakob Wasserthal"
37
- }
38
- }
39
- ```
40
-
41
- ### Extended Fields
42
-
43
- Custom fields in `supportingData`:
44
-
45
- ```json
46
- {
47
- "supportingData": {
48
- "modalities": ["CT", "MRI"],
49
- "dimensions": ["3D"],
50
- "formats": ["DICOM", "NIfTI", "PNG"],
51
- "tasks": ["segmentation", "organ-segmentation"],
52
- "demo_url": "https://huggingface.co/spaces/username/totalsegmentator",
53
- "paper_url": "https://doi.org/10.1000/example",
54
- "citations": 150,
55
- "github_stars": 1200
56
- }
57
- }
58
- ```
59
-
60
- ### Field Descriptions
61
-
62
- #### name
63
- Canonical tool name (matches repository or published name)
64
-
65
- **Example**: `"TotalSegmentator"`, `"nnU-Net"`, `"MedSAM"`
66
-
67
- #### description
68
- Brief description of tool's purpose and capabilities
69
-
70
- **Guidelines**:
71
-
72
- - 1-2 sentences
73
- - Mention key features
74
- - Include domain/modality if specific
75
-
76
- #### url
77
- Primary landing page (usually GitHub repo)
78
-
79
- #### codeRepository
80
- Source code repository URL (GitHub, GitLab, etc.)
81
-
82
- #### programmingLanguage
83
- Primary language(s)
84
-
85
- **Common values**: `"Python"`, `"C++"`, `"JavaScript"`, `"Jupyter Notebook"`
86
-
87
- #### license
88
- Software license identifier (SPDX format)
89
-
90
- **Common values**:
91
-
92
- - `"Apache-2.0"`: Permissive, commercial OK
93
- - `"MIT"`: Very permissive
94
- - `"GPL-3.0"`: Copyleft
95
- - `"BSD-3-Clause"`: Permissive
96
- - `"Proprietary"`: Restricted
97
-
98
- #### keywords
99
- Array of relevant tags/keywords
100
-
101
- **Categories**:
102
-
103
- - **Tasks**: segmentation, classification, registration, detection
104
- - **Modalities**: CT, MRI, X-ray, ultrasound, microscopy
105
- - **Techniques**: deep-learning, traditional-cv, machine-learning
106
- - **Domains**: medical-imaging, scientific-imaging, neuroscience
107
-
108
- #### supportingData.modalities
109
- Medical imaging modalities supported
110
-
111
- **Standard values**:
112
-
113
- - `"CT"`: Computed Tomography
114
- - `"MRI"`: Magnetic Resonance Imaging
115
- - `"XR"`: X-ray radiography
116
- - `"US"`: Ultrasound
117
- - `"PET"`: Positron Emission Tomography
118
- - `"SPECT"`: Single-Photon Emission CT
119
- - `"OCT"`: Optical Coherence Tomography
120
- - `"Microscopy"`: Various microscopy types
121
-
122
- #### supportingData.dimensions
123
- Spatial dimensions supported
124
-
125
- **Values**: `["2D"]`, `["3D"]`, `["2D", "3D"]`, `["4D"]`
126
-
127
- - **2D**: Single slice images
128
- - **3D**: Volumetric data
129
- - **4D**: Time-series volumes (3D + time)
130
-
131
- #### supportingData.formats
132
- File formats supported for input/output
133
-
134
- **Common values**:
135
-
136
- - Medical: `"DICOM"`, `"NIfTI"`, `"NRRD"`, `"Analyze"`
137
- - Standard: `"PNG"`, `"JPEG"`, `"TIFF"`, `"BMP"`
138
- - Scientific: `"HDF5"`, `"Zarr"`, `"OME-TIFF"`
139
- - Other: `"NumPy"`, `"MAT"`
140
-
141
- #### supportingData.tasks
142
- Analysis tasks the tool performs
143
-
144
- **Common values**:
145
-
146
- - `"segmentation"`: Image segmentation
147
- - `"classification"`: Image classification
148
- - `"detection"`: Object detection
149
- - `"registration"`: Image registration/alignment
150
- - `"reconstruction"`: 3D reconstruction
151
- - `"enhancement"`: Image enhancement
152
- - `"analysis"`: General analysis
153
-
154
- #### supportingData.demo_url
155
- Link to runnable demo (HuggingFace Space, Colab, web app)
156
-
157
- **Preferred**: HuggingFace Gradio Spaces (best integration)
158
-
159
- **Example**: `"https://huggingface.co/spaces/username/toolname"`
160
-
161
- ## Catalog Structure
162
-
163
- ### File Format
164
-
165
- JSON Lines (JSONL): Each line is a complete JSON object
166
-
167
- ```jsonl
168
- {"@type": "SoftwareSourceCode", "name": "Tool1", ...}
169
- {"@type": "SoftwareSourceCode", "name": "Tool2", ...}
170
- {"@type": "SoftwareSourceCode", "name": "Tool3", ...}
171
- ```
172
-
173
- **Benefits**:
174
-
175
- - Easy to append new tools
176
- - Stream processing for large catalogs
177
- - Each line independently parseable
178
- - Git-friendly (line-based diffs)
179
-
180
- ### Catalog Loading
181
-
182
- ```python
183
- import json
184
-
185
- def load_catalog(path: str) -> list[dict]:
186
- tools = []
187
- with open(path) as f:
188
- for line in f:
189
- if line.strip():
190
- tools.append(json.loads(line))
191
- return tools
192
- ```
193
-
194
- ### Validation
195
-
196
- Tools are validated on load:
197
-
198
- ```python
199
- from pydantic import BaseModel, HttpUrl
200
-
201
- class SoftwareSourceCode(BaseModel):
202
- name: str
203
- description: str
204
- url: HttpUrl
205
- license: str
206
- keywords: list[str]
207
- supportingData: dict
208
-
209
- class Config:
210
- extra = "allow" # Allow additional schema.org fields
211
- ```
212
-
213
- ## Catalog Management
214
-
215
- ### Adding New Tools
216
-
217
- 1. **Create entry** following schema:
218
-
219
- ```json
220
- {
221
- "@type": "SoftwareSourceCode",
222
- "name": "NewTool",
223
- "description": "Brief description of the tool",
224
- "url": "https://github.com/user/newtool",
225
- "codeRepository": "https://github.com/user/newtool",
226
- "programmingLanguage": "Python",
227
- "license": "MIT",
228
- "keywords": ["segmentation", "CT"],
229
- "supportingData": {
230
- "modalities": ["CT"],
231
- "dimensions": ["3D"],
232
- "formats": ["DICOM", "NIfTI"],
233
- "tasks": ["segmentation"],
234
- "demo_url": "https://huggingface.co/spaces/user/newtool"
235
- }
236
- }
237
- ```
238
-
239
- 2. **Append to catalog.jsonl** (as single line, no pretty printing)
240
-
241
- 3. **Update checksum**:
242
-
243
- ```bash
244
- shasum dataset/catalog.jsonl > dataset/catalog.jsonl.sha1
245
- ```
246
-
247
- 4. **Sync catalog**:
248
-
249
- ```bash
250
- ai_agent sync
251
- ```
252
-
253
- This rebuilds the embeddings and FAISS index.
254
-
255
- ### Updating Existing Tools
256
-
257
- 1. **Find tool** in `catalog.jsonl`
258
- 2. **Edit JSON** (update fields)
259
- 3. **Validate JSON** syntax
260
- 4. **Update checksum** and **sync**
261
-
262
- ### Removing Tools
263
-
264
- 1. **Delete line** from `catalog.jsonl`
265
- 2. **Update checksum** and **sync**
266
-
267
- <!-- ## Catalog Sources
268
-
269
- ### Current Catalog
270
-
271
- Built from:
272
- - **Medical Imaging Tools**: TotalSegmentator, nnU-Net, MedSAM, etc.
273
- - **Computer Vision Libraries**: OpenCV, scikit-image
274
- - **Deep Learning Frameworks**: PyTorch, TensorFlow tools
275
- - **Specialized Tools**: ITK, SimpleITK, 3D Slicer modules
276
- - **HuggingFace Spaces**: Gradio apps for imaging
277
-
278
- ### Curation Process
279
-
280
- Tools are included based on:
281
- 1. **Relevance**: Imaging analysis tasks
282
- 2. **Quality**: Actively maintained, documented
283
- 3. **Accessibility**: Open-source or free demos
284
- 4. **Runnable**: Has demo or clear usage examples
285
-
286
- ### Catalog Growth
287
-
288
- **Current**: ~150 tools
289
- **Target**: 500+ tools covering:
290
- - Medical imaging (CT, MRI, X-ray, ultrasound, pathology)
291
- - Scientific imaging (microscopy, astronomy, remote sensing)
292
- - Computer vision (general object detection, segmentation, etc.) -->
293
-
294
- ## Synchronization
295
-
296
- ### Auto-Sync
297
-
298
- Configured via `.env`:
299
-
300
- ```dotenv
301
- SYNC_EVERY_HOURS=24
302
- ```
303
-
304
- **Process**:
305
- 1. Background thread checks catalog every 24h
306
- 2. Compares SHA1 checksum
307
- 3. If changed:
308
- - Reload catalog
309
- - Re-embed all tools
310
- - Rebuild FAISS index
311
-
312
- ### Manual Sync
313
-
314
- ```bash
315
- ai_agent sync
316
- ```
317
-
318
- **Output**:
319
- ```
320
- [sync] 150 → dataset/catalog.jsonl
321
- [sync] Rebuilding embeddings...
322
- [sync] Embedding 150 tools... (5.2s)
323
- [sync] Building FAISS index...
324
- [sync] Saved to artifacts/rag_index/
325
- [sync] Sync complete.
326
- ```
327
-
328
- ## Embeddings and Index
329
-
330
- ### Embedding Process
331
-
332
- For each tool, create text representation:
333
-
334
- ```python
335
- tool_text = f"{tool['name']} {tool['description']} {' '.join(tool['keywords'])}"
336
-
337
- # Optional: Include supportingData
338
- if 'supportingData' in tool:
339
- sd = tool['supportingData']
340
- tool_text += f" {' '.join(sd.get('modalities', []))}"
341
- tool_text += f" {' '.join(sd.get('tasks', []))}"
342
-
343
- # Embed
344
- embedding = embedder.encode(tool_text, normalize_embeddings=True)
345
- ```
346
-
347
- ### Index Storage
348
-
349
- ```
350
- artifacts/rag_index/
351
- ├── index.faiss # FAISS IndexFlatIP
352
- └── meta.json # Tool IDs, config, timestamps
353
- ```
354
-
355
- **meta.json** structure:
356
-
357
- ```json
358
- {
359
- "tool_ids": ["tool1", "tool2", ...],
360
- "version": "1.0",
361
- "embedding_model": "BAAI/bge-m3",
362
- "embedding_dim": 1024,
363
- "num_tools": 150,
364
- "created_at": "2024-03-01T12:00:00Z",
365
- "catalog_sha1": "abc123..."
366
- }
367
- ```
368
-
369
- ## Quality Assurance
370
-
371
- ### Validation Rules
372
-
373
- 1. **Required fields**: name, description, url, license
374
- 2. **Valid URLs**: Well-formed HTTP/HTTPS URLs
375
- 3. **Standard licenses**: SPDX identifiers preferred
376
- 4. **Consistent keywords**: Use standard terminology
377
- 5. **Demo URLs**: Verify demos are live and accessible
378
-
379
- ### Automated Checks
380
-
381
- ```python
382
- def validate_catalog(catalog_path):
383
- errors = []
384
-
385
- with open(catalog_path) as f:
386
- for i, line in enumerate(f, 1):
387
- try:
388
- tool = json.loads(line)
389
-
390
- # Required fields
391
- for field in ['name', 'description', 'url']:
392
- if field not in tool:
393
- errors.append(f"Line {i}: Missing {field}")
394
-
395
- # URL validation
396
- if not tool['url'].startswith('http'):
397
- errors.append(f"Line {i}: Invalid URL")
398
-
399
- # supportingData structure
400
- if 'supportingData' in tool:
401
- sd = tool['supportingData']
402
- if 'demo_url' in sd and sd['demo_url']:
403
- if not sd['demo_url'].startswith('http'):
404
- errors.append(f"Line {i}: Invalid demo_url")
405
-
406
- except json.JSONDecodeError as e:
407
- errors.append(f"Line {i}: JSON syntax error - {e}")
408
-
409
- return errors
410
- ```
411
-
412
- ## Best Practices
413
-
414
- ### Tool Descriptions
415
-
416
- ✅ **Good**:
417
- ```
418
- "Automated multi-organ segmentation for CT and MRI supporting 104 anatomical structures"
419
- ```
420
-
421
- ❌ **Bad**:
422
- ```
423
- "A tool" # Too vague
424
- "The best segmentation tool ever created with amazing accuracy..." # Too marketing-y
425
- ```
426
-
427
- ### Keywords
428
-
429
- ✅ **Good**:
430
- ```
431
- ["segmentation", "CT", "MRI", "medical-imaging", "deep-learning", "organ-segmentation"]
432
- ```
433
-
434
- ❌ **Bad**:
435
- ```
436
- ["cool", "awesome", "the best"] # Not searchable terms
437
- ```
438
-
439
- ### Demo URLs
440
-
441
- ✅ **Preferred**:
442
- - HuggingFace Gradio Spaces
443
- - Google Colab notebooks
444
- - Live web demos
445
-
446
- ❌ **Avoid**:
447
- - Dead links
448
- - Paywalled demos
449
- - Demos requiring registration
450
-
451
- ## Next Steps
452
-
453
- - Return to [Architecture Overview](overview.md)
454
- - Learn about [Retrieval Pipeline](retrieval.md)
455
- - Explore [Agent & VLM Selection](agent.md)
 
1
+ # Software Catalog
2
+
3
+ The software catalog is the foundation of the AI Imaging Agent, containing curated information about imaging analysis tools.
4
+
5
+ ## Overview
6
+
7
+ **Format**: JSON Lines (JSONL)
8
+ **Location**: `dataset/catalog.jsonl`
9
+ **Schema**: Based on schema.org SoftwareSourceCode
10
+ **Size**: ~150 tools currently
11
+
12
+ ## Catalog Schema
13
+
14
+ ### Core Fields
15
+
16
+ Based on [schema.org/SoftwareSourceCode](https://schema.org/SoftwareSourceCode):
17
+
18
+ ```json
19
+ {
20
+ "@type": "SoftwareSourceCode",
21
+ "name": "TotalSegmentator",
22
+ "description": "Tool for automated segmentation of 104 anatomical structures",
23
+ "url": "https://github.com/wasserth/TotalSegmentator",
24
+ "codeRepository": "https://github.com/wasserth/TotalSegmentator",
25
+ "programmingLanguage": "Python",
26
+ "runtimePlatform": "PyTorch",
27
+ "license": "Apache-2.0",
28
+ "keywords": ["segmentation", "CT", "MRI", "medical-imaging"],
29
+ "applicationCategory": "Medical Imaging",
30
+ "operatingSystem": ["Linux", "Windows", "macOS"],
31
+ "softwareVersion": "2.0.0",
32
+ "datePublished": "2022-09-01",
33
+ "dateModified": "2024-01-15",
34
+ "author": {
35
+ "@type": "Person",
36
+ "name": "Jakob Wasserthal"
37
+ }
38
+ }
39
+ ```
40
+
41
+ ### Extended Fields
42
+
43
+ Custom fields in `supportingData`:
44
+
45
+ ```json
46
+ {
47
+ "supportingData": {
48
+ "modalities": ["CT", "MRI"],
49
+ "dimensions": ["3D"],
50
+ "formats": ["DICOM", "NIfTI", "PNG"],
51
+ "tasks": ["segmentation", "organ-segmentation"],
52
+ "demo_url": "https://huggingface.co/spaces/username/totalsegmentator",
53
+ "paper_url": "https://doi.org/10.1000/example",
54
+ "citations": 150,
55
+ "github_stars": 1200
56
+ }
57
+ }
58
+ ```
59
+
60
+ ### Field Descriptions
61
+
62
+ #### name
63
+ Canonical tool name (matches repository or published name)
64
+
65
+ **Example**: `"TotalSegmentator"`, `"nnU-Net"`, `"MedSAM"`
66
+
67
+ #### description
68
+ Brief description of tool's purpose and capabilities
69
+
70
+ **Guidelines**:
71
+
72
+ - 1-2 sentences
73
+ - Mention key features
74
+ - Include domain/modality if specific
75
+
76
+ #### url
77
+ Primary landing page (usually GitHub repo)
78
+
79
+ #### codeRepository
80
+ Source code repository URL (GitHub, GitLab, etc.)
81
+
82
+ #### programmingLanguage
83
+ Primary language(s)
84
+
85
+ **Common values**: `"Python"`, `"C++"`, `"JavaScript"`, `"Jupyter Notebook"`
86
+
87
+ #### license
88
+ Software license identifier (SPDX format)
89
+
90
+ **Common values**:
91
+
92
+ - `"Apache-2.0"`: Permissive, commercial OK
93
+ - `"MIT"`: Very permissive
94
+ - `"GPL-3.0"`: Copyleft
95
+ - `"BSD-3-Clause"`: Permissive
96
+ - `"Proprietary"`: Restricted
97
+
98
+ #### keywords
99
+ Array of relevant tags/keywords
100
+
101
+ **Categories**:
102
+
103
+ - **Tasks**: segmentation, classification, registration, detection
104
+ - **Modalities**: CT, MRI, X-ray, ultrasound, microscopy
105
+ - **Techniques**: deep-learning, traditional-cv, machine-learning
106
+ - **Domains**: medical-imaging, scientific-imaging, neuroscience
107
+
108
+ #### supportingData.modalities
109
+ Medical imaging modalities supported
110
+
111
+ **Standard values**:
112
+
113
+ - `"CT"`: Computed Tomography
114
+ - `"MRI"`: Magnetic Resonance Imaging
115
+ - `"XR"`: X-ray radiography
116
+ - `"US"`: Ultrasound
117
+ - `"PET"`: Positron Emission Tomography
118
+ - `"SPECT"`: Single-Photon Emission CT
119
+ - `"OCT"`: Optical Coherence Tomography
120
+ - `"Microscopy"`: Various microscopy types
121
+
122
+ #### supportingData.dimensions
123
+ Spatial dimensions supported
124
+
125
+ **Values**: `["2D"]`, `["3D"]`, `["2D", "3D"]`, `["4D"]`
126
+
127
+ - **2D**: Single slice images
128
+ - **3D**: Volumetric data
129
+ - **4D**: Time-series volumes (3D + time)
130
+
131
+ #### supportingData.formats
132
+ File formats supported for input/output
133
+
134
+ **Common values**:
135
+
136
+ - Medical: `"DICOM"`, `"NIfTI"`, `"NRRD"`, `"Analyze"`
137
+ - Standard: `"PNG"`, `"JPEG"`, `"TIFF"`, `"BMP"`
138
+ - Scientific: `"HDF5"`, `"Zarr"`, `"OME-TIFF"`
139
+ - Other: `"NumPy"`, `"MAT"`
140
+
141
+ #### supportingData.tasks
142
+ Analysis tasks the tool performs
143
+
144
+ **Common values**:
145
+
146
+ - `"segmentation"`: Image segmentation
147
+ - `"classification"`: Image classification
148
+ - `"detection"`: Object detection
149
+ - `"registration"`: Image registration/alignment
150
+ - `"reconstruction"`: 3D reconstruction
151
+ - `"enhancement"`: Image enhancement
152
+ - `"analysis"`: General analysis
153
+
154
+ #### supportingData.demo_url
155
+ Link to runnable demo (HuggingFace Space, Colab, web app)
156
+
157
+ **Preferred**: HuggingFace Gradio Spaces (best integration)
158
+
159
+ **Example**: `"https://huggingface.co/spaces/username/toolname"`
160
+
161
+ ## Catalog Structure
162
+
163
+ ### File Format
164
+
165
+ JSON Lines (JSONL): Each line is a complete JSON object
166
+
167
+ ```jsonl
168
+ {"@type": "SoftwareSourceCode", "name": "Tool1", ...}
169
+ {"@type": "SoftwareSourceCode", "name": "Tool2", ...}
170
+ {"@type": "SoftwareSourceCode", "name": "Tool3", ...}
171
+ ```
172
+
173
+ **Benefits**:
174
+
175
+ - Easy to append new tools
176
+ - Stream processing for large catalogs
177
+ - Each line independently parseable
178
+ - Git-friendly (line-based diffs)
179
+
180
+ ### Catalog Loading
181
+
182
+ ```python
183
+ import json
184
+
185
+ def load_catalog(path: str) -> list[dict]:
186
+ tools = []
187
+ with open(path) as f:
188
+ for line in f:
189
+ if line.strip():
190
+ tools.append(json.loads(line))
191
+ return tools
192
+ ```
193
+
194
+ ### Validation
195
+
196
+ Tools are validated on load:
197
+
198
+ ```python
199
+ from pydantic import BaseModel, HttpUrl
200
+
201
+ class SoftwareSourceCode(BaseModel):
202
+ name: str
203
+ description: str
204
+ url: HttpUrl
205
+ license: str
206
+ keywords: list[str]
207
+ supportingData: dict
208
+
209
+ class Config:
210
+ extra = "allow" # Allow additional schema.org fields
211
+ ```
212
+
213
+ ## Catalog Management
214
+
215
+ ### Adding New Tools
216
+
217
+ 1. **Create entry** following schema:
218
+
219
+ ```json
220
+ {
221
+ "@type": "SoftwareSourceCode",
222
+ "name": "NewTool",
223
+ "description": "Brief description of the tool",
224
+ "url": "https://github.com/user/newtool",
225
+ "codeRepository": "https://github.com/user/newtool",
226
+ "programmingLanguage": "Python",
227
+ "license": "MIT",
228
+ "keywords": ["segmentation", "CT"],
229
+ "supportingData": {
230
+ "modalities": ["CT"],
231
+ "dimensions": ["3D"],
232
+ "formats": ["DICOM", "NIfTI"],
233
+ "tasks": ["segmentation"],
234
+ "demo_url": "https://huggingface.co/spaces/user/newtool"
235
+ }
236
+ }
237
+ ```
238
+
239
+ 2. **Append to catalog.jsonl** (as single line, no pretty printing)
240
+
241
+ 3. **Update checksum**:
242
+
243
+ ```bash
244
+ shasum dataset/catalog.jsonl > dataset/catalog.jsonl.sha1
245
+ ```
246
+
247
+ 4. **Sync catalog**:
248
+
249
+ ```bash
250
+ ai_agent sync
251
+ ```
252
+
253
+ This rebuilds the embeddings and FAISS index.
254
+
255
+ ### Updating Existing Tools
256
+
257
+ 1. **Find tool** in `catalog.jsonl`
258
+ 2. **Edit JSON** (update fields)
259
+ 3. **Validate JSON** syntax
260
+ 4. **Update checksum** and **sync**
261
+
262
+ ### Removing Tools
263
+
264
+ 1. **Delete line** from `catalog.jsonl`
265
+ 2. **Update checksum** and **sync**
266
+
267
+ <!-- ## Catalog Sources
268
+
269
+ ### Current Catalog
270
+
271
+ Built from:
272
+ - **Medical Imaging Tools**: TotalSegmentator, nnU-Net, MedSAM, etc.
273
+ - **Computer Vision Libraries**: OpenCV, scikit-image
274
+ - **Deep Learning Frameworks**: PyTorch, TensorFlow tools
275
+ - **Specialized Tools**: ITK, SimpleITK, 3D Slicer modules
276
+ - **HuggingFace Spaces**: Gradio apps for imaging
277
+
278
+ ### Curation Process
279
+
280
+ Tools are included based on:
281
+ 1. **Relevance**: Imaging analysis tasks
282
+ 2. **Quality**: Actively maintained, documented
283
+ 3. **Accessibility**: Open-source or free demos
284
+ 4. **Runnable**: Has demo or clear usage examples
285
+
286
+ ### Catalog Growth
287
+
288
+ **Current**: ~150 tools
289
+ **Target**: 500+ tools covering:
290
+ - Medical imaging (CT, MRI, X-ray, ultrasound, pathology)
291
+ - Scientific imaging (microscopy, astronomy, remote sensing)
292
+ - Computer vision (general object detection, segmentation, etc.) -->
293
+
294
+ ## Synchronization
295
+
296
+ ### Auto-Sync
297
+
298
+ Configured via `.env`:
299
+
300
+ ```dotenv
301
+ SYNC_EVERY_HOURS=24
302
+ ```
303
+
304
+ **Process**:
305
+ 1. Background thread checks catalog every 24h
306
+ 2. Compares SHA1 checksum
307
+ 3. If changed:
308
+ - Reload catalog
309
+ - Re-embed all tools
310
+ - Rebuild FAISS index
311
+
312
+ ### Manual Sync
313
+
314
+ ```bash
315
+ ai_agent sync
316
+ ```
317
+
318
+ **Output**:
319
+ ```
320
+ [sync] 150 → dataset/catalog.jsonl
321
+ [sync] Rebuilding embeddings...
322
+ [sync] Embedding 150 tools... (5.2s)
323
+ [sync] Building FAISS index...
324
+ [sync] Saved to artifacts/rag_index/
325
+ [sync] Sync complete.
326
+ ```
327
+
328
+ ## Embeddings and Index
329
+
330
+ ### Embedding Process
331
+
332
+ For each tool, create text representation:
333
+
334
+ ```python
335
+ tool_text = f"{tool['name']} {tool['description']} {' '.join(tool['keywords'])}"
336
+
337
+ # Optional: Include supportingData
338
+ if 'supportingData' in tool:
339
+ sd = tool['supportingData']
340
+ tool_text += f" {' '.join(sd.get('modalities', []))}"
341
+ tool_text += f" {' '.join(sd.get('tasks', []))}"
342
+
343
+ # Embed
344
+ embedding = embedder.encode(tool_text, normalize_embeddings=True)
345
+ ```
346
+
347
+ ### Index Storage
348
+
349
+ ```
350
+ artifacts/rag_index/
351
+ ├── index.faiss # FAISS IndexFlatIP
352
+ └── meta.json # Tool IDs, config, timestamps
353
+ ```
354
+
355
+ **meta.json** structure:
356
+
357
+ ```json
358
+ {
359
+ "tool_ids": ["tool1", "tool2", ...],
360
+ "version": "1.0",
361
+ "embedding_model": "BAAI/bge-m3",
362
+ "embedding_dim": 1024,
363
+ "num_tools": 150,
364
+ "created_at": "2024-03-01T12:00:00Z",
365
+ "catalog_sha1": "abc123..."
366
+ }
367
+ ```
368
+
369
+ ## Quality Assurance
370
+
371
+ ### Validation Rules
372
+
373
+ 1. **Required fields**: name, description, url, license
374
+ 2. **Valid URLs**: Well-formed HTTP/HTTPS URLs
375
+ 3. **Standard licenses**: SPDX identifiers preferred
376
+ 4. **Consistent keywords**: Use standard terminology
377
+ 5. **Demo URLs**: Verify demos are live and accessible
378
+
379
+ ### Automated Checks
380
+
381
+ ```python
382
+ def validate_catalog(catalog_path):
383
+ errors = []
384
+
385
+ with open(catalog_path) as f:
386
+ for i, line in enumerate(f, 1):
387
+ try:
388
+ tool = json.loads(line)
389
+
390
+ # Required fields
391
+ for field in ['name', 'description', 'url']:
392
+ if field not in tool:
393
+ errors.append(f"Line {i}: Missing {field}")
394
+
395
+ # URL validation
396
+ if not tool['url'].startswith('http'):
397
+ errors.append(f"Line {i}: Invalid URL")
398
+
399
+ # supportingData structure
400
+ if 'supportingData' in tool:
401
+ sd = tool['supportingData']
402
+ if 'demo_url' in sd and sd['demo_url']:
403
+ if not sd['demo_url'].startswith('http'):
404
+ errors.append(f"Line {i}: Invalid demo_url")
405
+
406
+ except json.JSONDecodeError as e:
407
+ errors.append(f"Line {i}: JSON syntax error - {e}")
408
+
409
+ return errors
410
+ ```
411
+
412
+ ## Best Practices
413
+
414
+ ### Tool Descriptions
415
+
416
+ ✅ **Good**:
417
+ ```
418
+ "Automated multi-organ segmentation for CT and MRI supporting 104 anatomical structures"
419
+ ```
420
+
421
+ ❌ **Bad**:
422
+ ```
423
+ "A tool" # Too vague
424
+ "The best segmentation tool ever created with amazing accuracy..." # Too marketing-y
425
+ ```
426
+
427
+ ### Keywords
428
+
429
+ ✅ **Good**:
430
+ ```
431
+ ["segmentation", "CT", "MRI", "medical-imaging", "deep-learning", "organ-segmentation"]
432
+ ```
433
+
434
+ ❌ **Bad**:
435
+ ```
436
+ ["cool", "awesome", "the best"] # Not searchable terms
437
+ ```
438
+
439
+ ### Demo URLs
440
+
441
+ ✅ **Preferred**:
442
+ - HuggingFace Gradio Spaces
443
+ - Google Colab notebooks
444
+ - Live web demos
445
+
446
+ ❌ **Avoid**:
447
+ - Dead links
448
+ - Paywalled demos
449
+ - Demos requiring registration
450
+
451
+ ## Next Steps
452
+
453
+ - Return to [Architecture Overview](overview.md)
454
+ - Learn about [Retrieval Pipeline](retrieval.md)
455
+ - Explore [Agent & VLM Selection](agent.md)
docs/architecture/overview.md CHANGED
@@ -1,459 +1,459 @@
1
- # Architecture Overview
2
-
3
- The AI Imaging Agent uses a **two-stage pipeline** that combines fast text retrieval with vision-language model selection to recommend imaging tools.
4
-
5
- ## System Architecture
6
-
7
- ```mermaid
8
- graph TB
9
- subgraph "User Interface"
10
- UI[Gradio Chat Interface]
11
- end
12
-
13
- subgraph "API Layer"
14
- Pipeline[RAGImagingPipeline]
15
- Validator[File Validator]
16
- MetaExtractor[Metadata Extractor]
17
- end
18
-
19
- subgraph "Stage 1: Retrieval"
20
- Embedder[BGE-M3 Text Embedder]
21
- FAISS[FAISS Vector Index]
22
- Reranker[CrossEncoder Reranker]
23
- Catalog[Software Catalog JSONL]
24
- end
25
-
26
- subgraph "Stage 2: Agent Selection"
27
- Agent[PydanticAI Agent]
28
- VLM[GPT-4o/4o-mini VLM]
29
- Tools[Agent Tools]
30
- end
31
-
32
- UI --> Pipeline
33
- Pipeline --> Validator
34
- Pipeline --> MetaExtractor
35
- Pipeline --> Embedder
36
- Embedder --> FAISS
37
- FAISS --> Reranker
38
- Catalog -.-> FAISS
39
- Reranker --> Agent
40
- Agent --> VLM
41
- Agent --> Tools
42
- Agent --> UI
43
- ```
44
-
45
- ## Design Principles
46
-
47
- ### 1. Two-Stage Pipeline
48
-
49
- **Why two stages?**
50
-
51
- - **Speed**: Text retrieval is fast (~100ms), VLM calls are slower (~2-5s)
52
- - **Cost**: Only run expensive VLM on top candidates
53
- - **Quality**: Combine semantic search (Stage 1) with reasoning (Stage 2)
54
-
55
- ### 2. No Generation in Retrieval
56
-
57
- Stage 1 uses **no LLMs**:
58
-
59
- - Deterministic text search
60
- - Reproducible results
61
- - Fast iteration
62
- - Lower cost
63
-
64
- ### 3. Single VLM Call in Selection
65
-
66
- Stage 2 makes **exactly one VLM call**:
67
-
68
- - Sees all candidates at once
69
- - Performs comparative reasoning
70
- - Returns complete rankings
71
- - Efficient use of context window
72
-
73
- ### 4. Vision + Text Integration
74
-
75
- VLM receives:
76
-
77
- - **Visual**: PNG preview of image
78
- - **Textual**: Query, metadata, candidate descriptions
79
- - **Structured**: Candidate metadata table
80
-
81
- Enables image-aware tool selection.
82
-
83
- ## Data Flow
84
-
85
- ### Input Processing
86
-
87
- ```
88
- User uploads: scan.dcm
89
- "Segment the lungs"
90
-
91
- ↓ File Validation
92
- - Size check (< 200MB for DICOM)
93
- - Format validation
94
- - Security checks
95
-
96
- ↓ Metadata Extraction
97
- - Format: DICOM
98
- - Modality: CT
99
- - Dimensions: 512×512×300 (3D)
100
- - Spacing: 0.7×0.7×1.5mm
101
-
102
- ↓ Preview Generation
103
- - Extract middle slice: scan_preview.png
104
- - Format: PNG, RGB
105
- - Preserve metadata separately
106
- ```
107
-
108
- ### Stage 1: Retrieval
109
-
110
- ```
111
- Query: "Segment the lungs"
112
- Uploaded: scan.dcm (DICOM, CT, 3D)
113
-
114
- ↓ Query Enhancement
115
- Enhanced: "Segment the lungs format:DICOM format:CT format:3D"
116
-
117
- ↓ Metadata-Aware Hinting
118
- + image metadata summary (modality/anatomy/dims)
119
-
120
- ↓ Embedding (BGE-M3)
121
- Vector: [0.23, -0.15, 0.87, ..., 0.34] # 1024 dims
122
-
123
- ↓ FAISS Search
124
- Top 20 candidates by cosine similarity
125
-
126
- ↓ Retry Broadening (if low results)
127
- retry with a shorter query formulation
128
-
129
- ↓ CrossEncoder Reranking
130
- Re-score with cross-attention
131
- Top 8 candidates
132
-
133
- → Candidates passed to Stage 2
134
- ```
135
-
136
- ### Stage 2: Agent Selection
137
-
138
- ```
139
- Inputs:
140
- - User query: "Segment the lungs"
141
- - Image preview: scan_preview.png
142
- - Candidates: [tool1, tool2, ..., tool8]
143
- - Metadata: DICOM, CT, 3D, 512×512×300
144
-
145
- ↓ VLM Prompt Construction
146
- System: "You are an imaging tool expert..."
147
- User text: Query + metadata + candidate table
148
- User image: PNG preview
149
-
150
- ↓ VLM Call (GPT-4o)
151
- - Analyzes image content (CT thorax)
152
- - Reads candidate descriptions
153
- - Considers format compatibility
154
- - Reasons about task alignment
155
-
156
- ↓ Response (Structured)
157
- {
158
- "status": "complete",
159
- "recommendations": [
160
- {
161
- "rank": 1,
162
- "name": "TotalSegmentator",
163
- "accuracy": 95,
164
- "explanation": "...",
165
- "reason": "task_match"
166
- },
167
- ...
168
- ]
169
- }
170
-
171
- → Formatted recommendations to user
172
- ```
173
-
174
- ## Key Components
175
-
176
- ### api/pipeline.py
177
-
178
- **RAGImagingPipeline**: Main orchestrator
179
-
180
- ```python
181
- class RAGImagingPipeline:
182
- def __init__(self, catalog_path, index_dir):
183
- self.retriever = TextRetriever(...)
184
- # Stage 2 (selection/ranking) is handled by the PydanticAI agent
185
- # configured in generator/prompts.py using models from generator/schema.py
186
-
187
- def recommend(self, query, files):
188
- # Stage 1: Retrieval
189
- candidates = self.retriever.retrieve(query)
190
-
191
- # Stage 2: Selection via PydanticAI agent
192
- recommendations = run_selection_agent(
193
- query=query,
194
- candidates=candidates,
195
- files=files,
196
- )
197
-
198
- return recommendations
199
- ```
200
-
201
- **Responsibilities**:
202
-
203
- - File validation
204
- - Metadata extraction
205
- - Pipeline orchestration
206
- - Error handling
207
-
208
- ### retriever/
209
-
210
- **Text-based retrieval, no LLMs**
211
-
212
- Components:
213
-
214
- - `text_embedder.py`: BGE-M3 embedding model
215
- - `vector_index.py`: FAISS index management
216
- - `reranker.py`: CrossEncoder reranking
217
- - `software_doc.py`: Catalog schema and loading
218
-
219
- **Retrieval flow**:
220
-
221
- 1. Embed query → vector
222
- 2. FAISS search → top-N by similarity
223
- 3. CrossEncoder → rerank with cross-attention
224
- 4. Return top-K candidates
225
-
226
- ### generator/
227
-
228
- **VLM-based tool selection building blocks**
229
-
230
- Components:
231
-
232
- - `schema.py`: Pydantic models for agent responses and tool recommendations
233
- - `prompts.py`: System and tool-selection prompts used by the PydanticAI agent
234
-
235
- **Selection logic**:
236
-
237
- - Implemented in the PydanticAI agent (`agent/agent.py`) using these schemas and prompts
238
- - Single VLM call with all candidates
239
- - Structured output (Pydantic schemas) with ranked recommendations
240
- - Vision + text multimodal input
241
-
242
- ### agent/
243
-
244
- **PydanticAI conversational agent**
245
-
246
- Components:
247
-
248
- - `agent.py`: Agent definition and tools
249
- - `state.py`: ChatState dataclass
250
- - `tools.py`: Agent tools (search, repo_info, demo_exec)
251
-
252
- **Tools**:
253
-
254
- - `search_alternative`: Request alternative search
255
- - `repo_info`: Fetch GitHub repository details
256
- - `run_gradio_demo`: Execute Gradio Space demos
257
-
258
- ### utils/
259
-
260
- **Shared utilities**
261
-
262
- - `image_meta.py`: DICOM/NIfTI/TIFF metadata extraction
263
- - `file_validator.py`: Size and format validation
264
- - `previews.py`: Image conversion to PNG
265
- - `tags.py`: Control tag parsing (`[EXCLUDE:...]`, etc.)
266
- - `config.py`: Configuration management
267
-
268
- ### ui/
269
-
270
- **Gradio interface**
271
-
272
- Components:
273
-
274
- - `app.py`: Gradio application
275
- - `components.py`: Reusable UI components
276
- - `handlers.py`: Message handlers
277
- - `formatters.py`: Response formatting
278
- - `visualizations.py`: Previews and traces
279
-
280
- **Key function**:
281
- ```python
282
- def respond(message: str, files: list, state: dict) -> tuple:
283
- """
284
- Main interaction function.
285
-
286
- Returns: (reply, media, updated_state)
287
- """
288
- ```
289
-
290
- ## Module Boundaries
291
-
292
- Clear separation of concerns:
293
-
294
- | Module | Purpose | Dependencies |
295
- |--------|---------|--------------|
296
- | `api/` | Pipeline orchestration | `retriever/`, `generator/`, `utils/` |
297
- | `retriever/` | Text search only | None (pure retrieval) |
298
- | `generator/` | VLM selection only | None (pure generation) |
299
- | `agent/` | Conversational logic | `api/`, `utils/` |
300
- | `ui/` | Interface only | `agent/`, `api/` |
301
- | `utils/` | Shared functionality | None (pure utilities) |
302
-
303
- **Benefits**:
304
-
305
- - Independent testing
306
- - Clear interfaces
307
- - Modular replacement
308
- - No circular dependencies
309
-
310
- ## Data Schemas
311
-
312
- ### Software Catalog
313
-
314
- JSONL format, based on schema.org SoftwareSourceCode:
315
-
316
- ```json
317
- {
318
- "name": "TotalSegmentator",
319
- "description": "Automated multi-organ segmentation...",
320
- "url": "https://github.com/wasserth/TotalSegmentator",
321
- "codeRepository": "https://github.com/wasserth/TotalSegmentator",
322
- "programmingLanguage": "Python",
323
- "license": "Apache-2.0",
324
- "keywords": ["segmentation", "medical-imaging", "CT"],
325
- "applicationCategory": "Medical Imaging",
326
- "operatingSystem": ["Linux", "Windows", "macOS"],
327
- "softwareRequirements": ["Python 3.9+", "PyTorch"],
328
- "supportingData": {
329
- "modalities": ["CT", "MRI"],
330
- "dimensions": ["3D"],
331
- "formats": ["DICOM", "NIfTI"],
332
- "tasks": ["segmentation"],
333
- "demo_url": "https://huggingface.co/spaces/..."
334
- }
335
- }
336
- ```
337
-
338
- ### Agent Response
339
-
340
- Pydantic models in `generator/schema.py`:
341
-
342
- ```python
343
- class ToolRecommendation(BaseModel):
344
- rank: int
345
- name: str
346
- accuracy_score: int # 0-100
347
- explanation: str
348
- reason: ToolReason # Enum
349
- supporting_data: dict
350
-
351
- class AgentResponse(BaseModel):
352
- status: ConversationStatus # Enum
353
- recommendations: list[ToolRecommendation]
354
- message: str | None
355
- ```
356
-
357
- **Validation**:
358
-
359
- - Type checking via Pydantic
360
- - Enum constraints
361
- - Field aliases for LLM compatibility
362
-
363
- ## Extension Points
364
-
365
- ### Adding New Models
366
-
367
- In `config.yaml`:
368
-
369
- ```yaml
370
- available_models:
371
- - display_name: "Custom Model"
372
- name: "model-name"
373
- base_url: "https://api.example.com/v1"
374
- api_key_env: "CUSTOM_API_KEY"
375
- ```
376
-
377
- ### Adding New Tools
378
-
379
- Add a tool in `agent/agent.py` and route implementation to `agent/tools/` modules:
380
-
381
- ```python
382
- @agent.tool
383
- async def new_tool(ctx: RunContext[AgentState], param: str) -> str:
384
- """Tool description for the agent."""
385
- # Delegate to ai_agent.agent.tools.* implementation
386
- return result
387
- ```
388
-
389
- ### Custom Metadata Extractors
390
-
391
- In `utils/image_meta.py`:
392
-
393
- ```python
394
- def extract_custom_format(file_path: str) -> dict:
395
- """Extract metadata from custom format."""
396
- # Implementation
397
- return metadata
398
- ```
399
-
400
- <!-- ## Performance Characteristics
401
-
402
- ### Latency Breakdown
403
-
404
- Typical request (~3-5 seconds total):
405
-
406
- | Stage | Time | Notes |
407
- |-------|------|-------|
408
- | File upload | 100-500ms | Network + validation |
409
- | Metadata extraction | 50-200ms | Format-dependent |
410
- | Preview generation | 100-500ms | Image conversion |
411
- | Retrieval (Stage 1) | 100-200ms | Embedding + FAISS |
412
- | Reranking | 200-500ms | CrossEncoder |
413
- | VLM call (Stage 2) | 2-4s | OpenAI API |
414
- | Response formatting | 50ms | JSON → UI |
415
-
416
- **Bottleneck**: VLM API call (Stage 2)
417
-
418
- ### Scalability
419
-
420
- **Current**:
421
-
422
- - Single-user Gradio app
423
- - In-memory FAISS index
424
- - Synchronous processing
425
-
426
- **Production considerations**:
427
-
428
- - FastAPI backend for multi-user
429
- - Async VLM calls
430
- - Redis for session state
431
- - CDN for catalog + index -->
432
-
433
- ## Security Considerations
434
-
435
- ### User Data
436
-
437
- - **Images**: Sent to OpenAI API (preview PNG) if gpt is selected
438
- - **Metadata**: Processed locally, sent to VLM as text
439
- - **Queries**: Sent to OpenAI API
440
-
441
- **Privacy**: User data sees OpenAI's VLM API only.
442
-
443
- ### Catalog Integrity
444
-
445
- - Software catalog is curated
446
- - SHA1 checksums verify integrity
447
- - No user-generated catalog entries
448
-
449
- ### Demo Execution
450
-
451
- - Calls external Gradio Spaces (user choice)
452
- - No credentials shared with demos
453
- - User's image uploaded to public spaces (warn users)
454
-
455
- ## Next Steps
456
-
457
- - Deep dive into [Retrieval Pipeline](retrieval.md)
458
- - Learn about [Agent & VLM Selection](agent.md)
459
- - Explore [Software Catalog](catalog.md)
 
1
+ # Architecture Overview
2
+
3
+ The AI Imaging Agent uses a **two-stage pipeline** that combines fast text retrieval with vision-language model selection to recommend imaging tools.
4
+
5
+ ## System Architecture
6
+
7
+ ```mermaid
8
+ graph TB
9
+ subgraph "User Interface"
10
+ UI[Gradio Chat Interface]
11
+ end
12
+
13
+ subgraph "API Layer"
14
+ Pipeline[RAGImagingPipeline]
15
+ Validator[File Validator]
16
+ MetaExtractor[Metadata Extractor]
17
+ end
18
+
19
+ subgraph "Stage 1: Retrieval"
20
+ Embedder[BGE-M3 Text Embedder]
21
+ FAISS[FAISS Vector Index]
22
+ Reranker[CrossEncoder Reranker]
23
+ Catalog[Software Catalog JSONL]
24
+ end
25
+
26
+ subgraph "Stage 2: Agent Selection"
27
+ Agent[PydanticAI Agent]
28
+ VLM[GPT-4o/4o-mini VLM]
29
+ Tools[Agent Tools]
30
+ end
31
+
32
+ UI --> Pipeline
33
+ Pipeline --> Validator
34
+ Pipeline --> MetaExtractor
35
+ Pipeline --> Embedder
36
+ Embedder --> FAISS
37
+ FAISS --> Reranker
38
+ Catalog -.-> FAISS
39
+ Reranker --> Agent
40
+ Agent --> VLM
41
+ Agent --> Tools
42
+ Agent --> UI
43
+ ```
44
+
45
+ ## Design Principles
46
+
47
+ ### 1. Two-Stage Pipeline
48
+
49
+ **Why two stages?**
50
+
51
+ - **Speed**: Text retrieval is fast (~100ms), VLM calls are slower (~2-5s)
52
+ - **Cost**: Only run expensive VLM on top candidates
53
+ - **Quality**: Combine semantic search (Stage 1) with reasoning (Stage 2)
54
+
55
+ ### 2. No Generation in Retrieval
56
+
57
+ Stage 1 uses **no LLMs**:
58
+
59
+ - Deterministic text search
60
+ - Reproducible results
61
+ - Fast iteration
62
+ - Lower cost
63
+
64
+ ### 3. Single VLM Call in Selection
65
+
66
+ Stage 2 makes **exactly one VLM call**:
67
+
68
+ - Sees all candidates at once
69
+ - Performs comparative reasoning
70
+ - Returns complete rankings
71
+ - Efficient use of context window
72
+
73
+ ### 4. Vision + Text Integration
74
+
75
+ VLM receives:
76
+
77
+ - **Visual**: PNG preview of image
78
+ - **Textual**: Query, metadata, candidate descriptions
79
+ - **Structured**: Candidate metadata table
80
+
81
+ Enables image-aware tool selection.
82
+
83
+ ## Data Flow
84
+
85
+ ### Input Processing
86
+
87
+ ```
88
+ User uploads: scan.dcm
89
+ "Segment the lungs"
90
+
91
+ ↓ File Validation
92
+ - Size check (< 200MB for DICOM)
93
+ - Format validation
94
+ - Security checks
95
+
96
+ ↓ Metadata Extraction
97
+ - Format: DICOM
98
+ - Modality: CT
99
+ - Dimensions: 512×512×300 (3D)
100
+ - Spacing: 0.7×0.7×1.5mm
101
+
102
+ ↓ Preview Generation
103
+ - Extract middle slice: scan_preview.png
104
+ - Format: PNG, RGB
105
+ - Preserve metadata separately
106
+ ```
107
+
108
+ ### Stage 1: Retrieval
109
+
110
+ ```
111
+ Query: "Segment the lungs"
112
+ Uploaded: scan.dcm (DICOM, CT, 3D)
113
+
114
+ ↓ Query Enhancement
115
+ Enhanced: "Segment the lungs format:DICOM format:CT format:3D"
116
+
117
+ ↓ Metadata-Aware Hinting
118
+ + image metadata summary (modality/anatomy/dims)
119
+
120
+ ↓ Embedding (BGE-M3)
121
+ Vector: [0.23, -0.15, 0.87, ..., 0.34] # 1024 dims
122
+
123
+ ↓ FAISS Search
124
+ Top 20 candidates by cosine similarity
125
+
126
+ ↓ Retry Broadening (if low results)
127
+ retry with a shorter query formulation
128
+
129
+ ↓ CrossEncoder Reranking
130
+ Re-score with cross-attention
131
+ Top 8 candidates
132
+
133
+ → Candidates passed to Stage 2
134
+ ```
135
+
136
+ ### Stage 2: Agent Selection
137
+
138
+ ```
139
+ Inputs:
140
+ - User query: "Segment the lungs"
141
+ - Image preview: scan_preview.png
142
+ - Candidates: [tool1, tool2, ..., tool8]
143
+ - Metadata: DICOM, CT, 3D, 512×512×300
144
+
145
+ ↓ VLM Prompt Construction
146
+ System: "You are an imaging tool expert..."
147
+ User text: Query + metadata + candidate table
148
+ User image: PNG preview
149
+
150
+ ↓ VLM Call (GPT-4o)
151
+ - Analyzes image content (CT thorax)
152
+ - Reads candidate descriptions
153
+ - Considers format compatibility
154
+ - Reasons about task alignment
155
+
156
+ ↓ Response (Structured)
157
+ {
158
+ "status": "complete",
159
+ "recommendations": [
160
+ {
161
+ "rank": 1,
162
+ "name": "TotalSegmentator",
163
+ "accuracy": 95,
164
+ "explanation": "...",
165
+ "reason": "task_match"
166
+ },
167
+ ...
168
+ ]
169
+ }
170
+
171
+ → Formatted recommendations to user
172
+ ```
173
+
174
+ ## Key Components
175
+
176
+ ### api/pipeline.py
177
+
178
+ **RAGImagingPipeline**: Main orchestrator
179
+
180
+ ```python
181
+ class RAGImagingPipeline:
182
+ def __init__(self, catalog_path, index_dir):
183
+ self.retriever = TextRetriever(...)
184
+ # Stage 2 (selection/ranking) is handled by the PydanticAI agent
185
+ # configured in generator/prompts.py using models from generator/schema.py
186
+
187
+ def recommend(self, query, files):
188
+ # Stage 1: Retrieval
189
+ candidates = self.retriever.retrieve(query)
190
+
191
+ # Stage 2: Selection via PydanticAI agent
192
+ recommendations = run_selection_agent(
193
+ query=query,
194
+ candidates=candidates,
195
+ files=files,
196
+ )
197
+
198
+ return recommendations
199
+ ```
200
+
201
+ **Responsibilities**:
202
+
203
+ - File validation
204
+ - Metadata extraction
205
+ - Pipeline orchestration
206
+ - Error handling
207
+
208
+ ### retriever/
209
+
210
+ **Text-based retrieval, no LLMs**
211
+
212
+ Components:
213
+
214
+ - `text_embedder.py`: BGE-M3 embedding model
215
+ - `vector_index.py`: FAISS index management
216
+ - `reranker.py`: CrossEncoder reranking
217
+ - `software_doc.py`: Catalog schema and loading
218
+
219
+ **Retrieval flow**:
220
+
221
+ 1. Embed query → vector
222
+ 2. FAISS search → top-N by similarity
223
+ 3. CrossEncoder → rerank with cross-attention
224
+ 4. Return top-K candidates
225
+
226
+ ### generator/
227
+
228
+ **VLM-based tool selection building blocks**
229
+
230
+ Components:
231
+
232
+ - `schema.py`: Pydantic models for agent responses and tool recommendations
233
+ - `prompts.py`: System and tool-selection prompts used by the PydanticAI agent
234
+
235
+ **Selection logic**:
236
+
237
+ - Implemented in the PydanticAI agent (`agent/agent.py`) using these schemas and prompts
238
+ - Single VLM call with all candidates
239
+ - Structured output (Pydantic schemas) with ranked recommendations
240
+ - Vision + text multimodal input
241
+
242
+ ### agent/
243
+
244
+ **PydanticAI conversational agent**
245
+
246
+ Components:
247
+
248
+ - `agent.py`: Agent definition and tools
249
+ - `state.py`: ChatState dataclass
250
+ - `tools.py`: Agent tools (search, repo_info, demo_exec)
251
+
252
+ **Tools**:
253
+
254
+ - `search_alternative`: Request alternative search
255
+ - `repo_info`: Fetch GitHub repository details
256
+ - `run_gradio_demo`: Execute Gradio Space demos
257
+
258
+ ### utils/
259
+
260
+ **Shared utilities**
261
+
262
+ - `image_meta.py`: DICOM/NIfTI/TIFF metadata extraction
263
+ - `file_validator.py`: Size and format validation
264
+ - `previews.py`: Image conversion to PNG
265
+ - `tags.py`: Control tag parsing (`[EXCLUDE:...]`, etc.)
266
+ - `config.py`: Configuration management
267
+
268
+ ### ui/
269
+
270
+ **Gradio interface**
271
+
272
+ Components:
273
+
274
+ - `app.py`: Gradio application
275
+ - `components.py`: Reusable UI components
276
+ - `handlers.py`: Message handlers
277
+ - `formatters.py`: Response formatting
278
+ - `visualizations.py`: Previews and traces
279
+
280
+ **Key function**:
281
+ ```python
282
+ def respond(message: str, files: list, state: dict) -> tuple:
283
+ """
284
+ Main interaction function.
285
+
286
+ Returns: (reply, media, updated_state)
287
+ """
288
+ ```
289
+
290
+ ## Module Boundaries
291
+
292
+ Clear separation of concerns:
293
+
294
+ | Module | Purpose | Dependencies |
295
+ |--------|---------|--------------|
296
+ | `api/` | Pipeline orchestration | `retriever/`, `generator/`, `utils/` |
297
+ | `retriever/` | Text search only | None (pure retrieval) |
298
+ | `generator/` | VLM selection only | None (pure generation) |
299
+ | `agent/` | Conversational logic | `api/`, `utils/` |
300
+ | `ui/` | Interface only | `agent/`, `api/` |
301
+ | `utils/` | Shared functionality | None (pure utilities) |
302
+
303
+ **Benefits**:
304
+
305
+ - Independent testing
306
+ - Clear interfaces
307
+ - Modular replacement
308
+ - No circular dependencies
309
+
310
+ ## Data Schemas
311
+
312
+ ### Software Catalog
313
+
314
+ JSONL format, based on schema.org SoftwareSourceCode:
315
+
316
+ ```json
317
+ {
318
+ "name": "TotalSegmentator",
319
+ "description": "Automated multi-organ segmentation...",
320
+ "url": "https://github.com/wasserth/TotalSegmentator",
321
+ "codeRepository": "https://github.com/wasserth/TotalSegmentator",
322
+ "programmingLanguage": "Python",
323
+ "license": "Apache-2.0",
324
+ "keywords": ["segmentation", "medical-imaging", "CT"],
325
+ "applicationCategory": "Medical Imaging",
326
+ "operatingSystem": ["Linux", "Windows", "macOS"],
327
+ "softwareRequirements": ["Python 3.9+", "PyTorch"],
328
+ "supportingData": {
329
+ "modalities": ["CT", "MRI"],
330
+ "dimensions": ["3D"],
331
+ "formats": ["DICOM", "NIfTI"],
332
+ "tasks": ["segmentation"],
333
+ "demo_url": "https://huggingface.co/spaces/..."
334
+ }
335
+ }
336
+ ```
337
+
338
+ ### Agent Response
339
+
340
+ Pydantic models in `generator/schema.py`:
341
+
342
+ ```python
343
+ class ToolRecommendation(BaseModel):
344
+ rank: int
345
+ name: str
346
+ accuracy_score: int # 0-100
347
+ explanation: str
348
+ reason: ToolReason # Enum
349
+ supporting_data: dict
350
+
351
+ class AgentResponse(BaseModel):
352
+ status: ConversationStatus # Enum
353
+ recommendations: list[ToolRecommendation]
354
+ message: str | None
355
+ ```
356
+
357
+ **Validation**:
358
+
359
+ - Type checking via Pydantic
360
+ - Enum constraints
361
+ - Field aliases for LLM compatibility
362
+
363
+ ## Extension Points
364
+
365
+ ### Adding New Models
366
+
367
+ In `config.yaml`:
368
+
369
+ ```yaml
370
+ available_models:
371
+ - display_name: "Custom Model"
372
+ name: "model-name"
373
+ base_url: "https://api.example.com/v1"
374
+ api_key_env: "CUSTOM_API_KEY"
375
+ ```
376
+
377
+ ### Adding New Tools
378
+
379
+ Add a tool in `agent/agent.py` and route implementation to `agent/tools/` modules:
380
+
381
+ ```python
382
+ @agent.tool
383
+ async def new_tool(ctx: RunContext[AgentState], param: str) -> str:
384
+ """Tool description for the agent."""
385
+ # Delegate to ai_agent.agent.tools.* implementation
386
+ return result
387
+ ```
388
+
389
+ ### Custom Metadata Extractors
390
+
391
+ In `utils/image_meta.py`:
392
+
393
+ ```python
394
+ def extract_custom_format(file_path: str) -> dict:
395
+ """Extract metadata from custom format."""
396
+ # Implementation
397
+ return metadata
398
+ ```
399
+
400
+ <!-- ## Performance Characteristics
401
+
402
+ ### Latency Breakdown
403
+
404
+ Typical request (~3-5 seconds total):
405
+
406
+ | Stage | Time | Notes |
407
+ |-------|------|-------|
408
+ | File upload | 100-500ms | Network + validation |
409
+ | Metadata extraction | 50-200ms | Format-dependent |
410
+ | Preview generation | 100-500ms | Image conversion |
411
+ | Retrieval (Stage 1) | 100-200ms | Embedding + FAISS |
412
+ | Reranking | 200-500ms | CrossEncoder |
413
+ | VLM call (Stage 2) | 2-4s | OpenAI API |
414
+ | Response formatting | 50ms | JSON → UI |
415
+
416
+ **Bottleneck**: VLM API call (Stage 2)
417
+
418
+ ### Scalability
419
+
420
+ **Current**:
421
+
422
+ - Single-user Gradio app
423
+ - In-memory FAISS index
424
+ - Synchronous processing
425
+
426
+ **Production considerations**:
427
+
428
+ - FastAPI backend for multi-user
429
+ - Async VLM calls
430
+ - Redis for session state
431
+ - CDN for catalog + index -->
432
+
433
+ ## Security Considerations
434
+
435
+ ### User Data
436
+
437
+ - **Images**: Sent to OpenAI API (preview PNG) if gpt is selected
438
+ - **Metadata**: Processed locally, sent to VLM as text
439
+ - **Queries**: Sent to OpenAI API
440
+
441
+ **Privacy**: User data sees OpenAI's VLM API only.
442
+
443
+ ### Catalog Integrity
444
+
445
+ - Software catalog is curated
446
+ - SHA1 checksums verify integrity
447
+ - No user-generated catalog entries
448
+
449
+ ### Demo Execution
450
+
451
+ - Calls external Gradio Spaces (user choice)
452
+ - No credentials shared with demos
453
+ - User's image uploaded to public spaces (warn users)
454
+
455
+ ## Next Steps
456
+
457
+ - Deep dive into [Retrieval Pipeline](retrieval.md)
458
+ - Learn about [Agent & VLM Selection](agent.md)
459
+ - Explore [Software Catalog](catalog.md)
docs/architecture/retrieval.md CHANGED
@@ -1,387 +1,387 @@
1
- # Retrieval Pipeline
2
-
3
- The retrieval stage is the first phase of the AI Imaging Agent's two-stage pipeline. It performs fast text-based search to find candidate tools from the software catalog.
4
-
5
- ## Overview
6
-
7
- **Goal**: Quickly narrow down the software catalog to most relevant candidates
8
-
9
- **Characteristics**:
10
-
11
- - ⚡ Fast (~100-300ms)
12
- - 🔢 Deterministic and reproducible
13
- - 🚫 No LLM calls
14
- - 💰 Low cost (no API fees)
15
-
16
- ## Pipeline Stages
17
-
18
- ```mermaid
19
- graph LR
20
- A[User Query] --> B[Query Enhancement]
21
- B --> C[Embedding]
22
- C --> D[FAISS Search]
23
- D --> E[CrossEncoder Rerank]
24
- E --> F[Top-K Candidates]
25
- ```
26
-
27
- ## Step 1: Query Enhancement
28
-
29
- ### Format Token Injection
30
-
31
- When users upload files, format tokens are added to the query:
32
-
33
- ```python
34
- # User uploads: scan.dcm
35
- # User query: "segment lungs"
36
-
37
- # Enhanced query:
38
- "segment lungs format:DICOM format:CT format:3D"
39
- ```
40
-
41
- **Format tokens added**:
42
-
43
- - File extension (`format:DICOM`, `format:NIfTI`)
44
- - Image modality from metadata (`format:CT`, `format:MRI`)
45
- - Dimensionality (`format:2D`, `format:3D`, `format:4D`)
46
-
47
- **Why this helps**:
48
-
49
- - Matches tools that support specific formats
50
- - Boosts DICOM-compatible tools for DICOM input
51
- - Ensures dimension compatibility (3D tools for volumes)
52
-
53
- ### Control Tag Processing
54
-
55
- Special tags are extracted and processed:
56
-
57
- ```python
58
- query = "segment lungs [EXCLUDE:tool1|tool2]"
59
-
60
- # Extracted:
61
- clean_query = "segment lungs"
62
- excluded_tools = ["tool1", "tool2"]
63
- ```
64
-
65
- **Supported tags**:
66
- - `[EXCLUDE:tool1|tool2]`: Filter tools from results
67
-
68
- ## Step 2: Metadata-Aware Querying
69
-
70
- The pipeline does not perform semantic vocabulary expansion. Instead, retrieval combines:
71
-
72
- - cleaned task text
73
- - format tokens inferred from uploaded files (for example `format:DICOM`)
74
- - compact image metadata hints (modality/anatomy/dimensionality when available)
75
-
76
- This keeps retrieval deterministic and closely tied to the user's data.
77
-
78
- ### Alternative Query Generation
79
-
80
- On retry (when initial results < 5 tools):
81
-
82
- ```python
83
- # Initial query
84
- query1 = "segment rare pulmonary structure"
85
- results = 2 tools # Too few!
86
-
87
- # Retry 1: Broader formulation (keep first 2-3 words)
88
- query2 = "segment rare pulmonary"
89
- results = 7 tools # Better!
90
-
91
- # Retry 2: If still insufficient, repeat with same broadening strategy
92
- ```
93
-
94
- **Max retries**: 2
95
-
96
- ## Step 3: Embedding
97
-
98
- ### BGE-M3 Model
99
-
100
- **Model**: `BAAI/bge-m3`
101
-
102
- **Characteristics**:
103
-
104
- - Multilingual (but used for English)
105
- - 1024-dimensional embeddings
106
- - Trained for retrieval tasks
107
- - Fast inference (~10ms per query)
108
-
109
- **Embedding process**:
110
-
111
- ```python
112
- from sentence_transformers import SentenceTransformer
113
-
114
- model = SentenceTransformer("BAAI/bge-m3")
115
- query_vector = model.encode(
116
- query,
117
- normalize_embeddings=True # L2 normalization for cosine similarity
118
- )
119
- # Returns: np.array of shape (1024,)
120
- ```
121
-
122
- ### Catalog Embedding
123
-
124
- Software tools are pre-embedded during indexing:
125
-
126
- ```python
127
- # For each tool in catalog:
128
- tool_text = f"{tool.name} {tool.description} {' '.join(tool.keywords)}"
129
- tool_vector = model.encode(tool_text, normalize_embeddings=True)
130
-
131
- # Store in FAISS index
132
- faiss_index.add(tool_vector)
133
- ```
134
-
135
- **Index structure**:
136
-
137
- - FAISS IndexFlatIP (inner product = cosine similarity for normalized vectors)
138
- - ~150 tools in current catalog
139
- - Index size: ~600KB
140
-
141
- ## Step 4: FAISS Search
142
-
143
- ### Vector Search
144
-
145
- FAISS performs fast similarity search:
146
-
147
- ```python
148
- import faiss
149
-
150
- # Search for top 20 most similar tools
151
- scores, indices = faiss_index.search(
152
- query_vector.reshape(1, -1),
153
- k=20
154
- )
155
-
156
- # Returns:
157
- # scores: [0.85, 0.82, 0.79, ...] # Cosine similarities
158
- # indices: [42, 17, 89, ...] # Tool IDs in catalog
159
- ```
160
-
161
- **Search algorithm**:
162
-
163
- - IndexFlatIP: Exact search (brute force)
164
- - Fast for catalog size (~150 tools)
165
- - Could use IVF for larger catalogs (>10k tools)
166
-
167
- **Why top-20**:
168
-
169
- - More candidates than needed (default final: 8)
170
- - Provides options for reranking
171
- - Balances recall vs. later stage cost
172
-
173
- ### Candidate Retrieval
174
-
175
- ```python
176
- candidates = [catalog[idx] for idx in indices[:20]]
177
- candidate_scores = scores[:20].tolist()
178
-
179
- # Example candidates:
180
- [
181
- {
182
- "name": "TotalSegmentator",
183
- "score": 0.85,
184
- "description": "Automated multi-organ segmentation...",
185
- ...
186
- },
187
- ...
188
- ]
189
- ```
190
-
191
- ## Step 5: CrossEncoder Reranking
192
-
193
- ### Why Rerank?
194
-
195
- **BiEncoder (BGE-M3)** limitations:
196
-
197
- - Encodes query and documents independently
198
- - No query-document interaction
199
- - Misses subtle relevance signals
200
-
201
- **CrossEncoder** benefits:
202
-
203
- - Jointly encodes query + document
204
- - Cross-attention between query and doc
205
- - More accurate relevance scoring
206
- - Slower (not suitable for entire catalog)
207
-
208
- ### Reranking Model
209
-
210
- **Model**: `BAAI/bge-reranker-v2-m3`
211
-
212
- **Characteristics**:
213
-
214
- - Trained on MS-MARCO passage ranking
215
- - 6 layers, fast inference (~50ms per pair)
216
- - Direct relevance score (no embedding)
217
-
218
- **Reranking process**:
219
-
220
- ```python
221
- from sentence_transformers import CrossEncoder
222
-
223
- reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")
224
-
225
- # Score each (query, candidate) pair
226
- pairs = [(query, candidate.description) for candidate in candidates]
227
- rerank_scores = reranker.predict(pairs)
228
-
229
- # Re-sort by rerank scores
230
- sorted_indices = np.argsort(rerank_scores)[::-1]
231
- reranked_candidates = [candidates[i] for i in sorted_indices][:8]
232
- ```
233
-
234
- **Output**: Top-8 candidates with refined ranking
235
-
236
- ## Output Format
237
-
238
- ### Candidate Schema
239
-
240
- Each candidate passed to Stage 2:
241
-
242
- ```python
243
- {
244
- "name": "TotalSegmentator",
245
- "description": "Automated multi-organ segmentation for CT and MRI",
246
- "url": "https://github.com/wasserth/TotalSegmentator",
247
- "keywords": ["segmentation", "medical-imaging", "CT", "MRI"],
248
- "license": "Apache-2.0",
249
- "supporting_data": {
250
- "modalities": ["CT", "MRI"],
251
- "dimensions": ["3D"],
252
- "formats": ["DICOM", "NIfTI"],
253
- "demo_url": "https://huggingface.co/spaces/..."
254
- },
255
- "retrieval_score": 0.85, # FAISS or rerank score
256
- }
257
- ```
258
-
259
- **Fields used by VLM**:
260
-
261
- - Essential for understanding tool capability
262
- - Formatted as table in VLM prompt
263
- - Enables comparative reasoning
264
-
265
- ## Index Management
266
-
267
- ### Building the Index
268
-
269
- Done during catalog sync:
270
-
271
- ```bash
272
- ai_agent sync
273
- ```
274
-
275
- **Process**:
276
-
277
- 1. Load catalog JSONL
278
- 2. Embed each tool description
279
- 3. Build FAISS index
280
- 4. Save to disk: `artifacts/rag_index/`
281
-
282
- **Files**:
283
- ```
284
- artifacts/rag_index/
285
- ├── index.faiss # FAISS binary index
286
- └── meta.json # Metadata (tool IDs, config)
287
- ```
288
-
289
- ### Loading the Index
290
-
291
- At startup:
292
-
293
- ```python
294
- from retriever.vector_index import VectorIndex
295
-
296
- index = VectorIndex()
297
- index.load("artifacts/rag_index")
298
-
299
- # Ready for queries
300
- results = index.search(query, k=20)
301
- ```
302
-
303
- ### Updating the Index
304
-
305
- When catalog changes:
306
- 1. Sync detects new/modified tools
307
- 2. Re-embed entire catalog (fast, ~2 seconds)
308
- 3. Rebuild FAISS index
309
- 4. Reload in pipeline (no restart needed)
310
-
311
- ## Performance Optimization
312
-
313
- ### Caching
314
-
315
- **Model loading**:
316
-
317
- - BGE-M3 and CrossEncoder loaded once at startup
318
- - Kept in memory for entire session
319
-
320
- **Index loading**:
321
-
322
- - FAISS index loaded once
323
- - Small enough to fit in memory (~MB)
324
-
325
- ### Batch Processing
326
-
327
- For multiple queries (testing, batch mode):
328
-
329
- ```python
330
- # Batch embed multiple queries
331
- query_vectors = model.encode(queries, batch_size=32)
332
-
333
- # Batch FAISS search
334
- scores, indices = index.search(query_vectors, k=20)
335
- ```
336
-
337
- ### GPU Acceleration
338
-
339
- Models can use GPU if available:
340
-
341
- ```python
342
- model = SentenceTransformer("BAAI/bge-m3", device="cuda")
343
- reranker = CrossEncoder("...", device="cuda")
344
- ```
345
-
346
- ## Retrieval Metrics
347
-
348
- ### Monitored Metrics
349
-
350
- During retrieval:
351
-
352
- - **Number of candidates found**: Should be ≥8 for good coverage
353
- - **Average similarity score**: Higher = better match
354
- - **Reranking impact**: Score change after reranking
355
- - **Retry usage**: Whether broadening retry was triggered
356
-
357
- ### Logging
358
-
359
- Retrieval events logged:
360
-
361
- ```
362
- INFO retriever.vector_index: FAISS search: query="segment lungs", results=20, top_score=0.85
363
- INFO retriever.reranker: Reranking 20 candidates, top_score_change=+0.12
364
- INFO retriever.pipeline: Final candidates: 8, avg_score=0.78
365
- ```
366
-
367
- ## Limitations
368
-
369
- ### Current Limitations
370
-
371
- 1. **English only**: No multilingual support (though model is capable)
372
- 2. **Small catalog**: ~150 tools (FAISS overkill, but scales)
373
- 3. **No filtering**: Can't filter by license, modality in retrieval (done in Stage 2)
374
- 4. **Heuristic retries**: Broadening strategy is simple prefix-based shortening
375
-
376
- ### Future Enhancements
377
-
378
- - **Hybrid search**: Combine semantic + keyword (BM25)
379
- - **Metadata filters**: Pre-filter by modality, license, format
380
- - **Personalization**: User history, preferences
381
- - **Adaptive retries**: Learn better broadening formulations from query logs
382
-
383
- ## Next Steps
384
-
385
- - Learn about [Agent & VLM Selection](agent.md)
386
- - Explore [Software Catalog](catalog.md)
387
- - Return to [Architecture Overview](overview.md)
 
1
+ # Retrieval Pipeline
2
+
3
+ The retrieval stage is the first phase of the AI Imaging Agent's two-stage pipeline. It performs fast text-based search to find candidate tools from the software catalog.
4
+
5
+ ## Overview
6
+
7
+ **Goal**: Quickly narrow down the software catalog to most relevant candidates
8
+
9
+ **Characteristics**:
10
+
11
+ - ⚡ Fast (~100-300ms)
12
+ - 🔢 Deterministic and reproducible
13
+ - 🚫 No LLM calls
14
+ - 💰 Low cost (no API fees)
15
+
16
+ ## Pipeline Stages
17
+
18
+ ```mermaid
19
+ graph LR
20
+ A[User Query] --> B[Query Enhancement]
21
+ B --> C[Embedding]
22
+ C --> D[FAISS Search]
23
+ D --> E[CrossEncoder Rerank]
24
+ E --> F[Top-K Candidates]
25
+ ```
26
+
27
+ ## Step 1: Query Enhancement
28
+
29
+ ### Format Token Injection
30
+
31
+ When users upload files, format tokens are added to the query:
32
+
33
+ ```python
34
+ # User uploads: scan.dcm
35
+ # User query: "segment lungs"
36
+
37
+ # Enhanced query:
38
+ "segment lungs format:DICOM format:CT format:3D"
39
+ ```
40
+
41
+ **Format tokens added**:
42
+
43
+ - File extension (`format:DICOM`, `format:NIfTI`)
44
+ - Image modality from metadata (`format:CT`, `format:MRI`)
45
+ - Dimensionality (`format:2D`, `format:3D`, `format:4D`)
46
+
47
+ **Why this helps**:
48
+
49
+ - Matches tools that support specific formats
50
+ - Boosts DICOM-compatible tools for DICOM input
51
+ - Ensures dimension compatibility (3D tools for volumes)
52
+
53
+ ### Control Tag Processing
54
+
55
+ Special tags are extracted and processed:
56
+
57
+ ```python
58
+ query = "segment lungs [EXCLUDE:tool1|tool2]"
59
+
60
+ # Extracted:
61
+ clean_query = "segment lungs"
62
+ excluded_tools = ["tool1", "tool2"]
63
+ ```
64
+
65
+ **Supported tags**:
66
+ - `[EXCLUDE:tool1|tool2]`: Filter tools from results
67
+
68
+ ## Step 2: Metadata-Aware Querying
69
+
70
+ The pipeline does not perform semantic vocabulary expansion. Instead, retrieval combines:
71
+
72
+ - cleaned task text
73
+ - format tokens inferred from uploaded files (for example `format:DICOM`)
74
+ - compact image metadata hints (modality/anatomy/dimensionality when available)
75
+
76
+ This keeps retrieval deterministic and closely tied to the user's data.
77
+
78
+ ### Alternative Query Generation
79
+
80
+ On retry (when initial results < 5 tools):
81
+
82
+ ```python
83
+ # Initial query
84
+ query1 = "segment rare pulmonary structure"
85
+ results = 2 tools # Too few!
86
+
87
+ # Retry 1: Broader formulation (keep first 2-3 words)
88
+ query2 = "segment rare pulmonary"
89
+ results = 7 tools # Better!
90
+
91
+ # Retry 2: If still insufficient, repeat with same broadening strategy
92
+ ```
93
+
94
+ **Max retries**: 2
95
+
96
+ ## Step 3: Embedding
97
+
98
+ ### BGE-M3 Model
99
+
100
+ **Model**: `BAAI/bge-m3`
101
+
102
+ **Characteristics**:
103
+
104
+ - Multilingual (but used for English)
105
+ - 1024-dimensional embeddings
106
+ - Trained for retrieval tasks
107
+ - Fast inference (~10ms per query)
108
+
109
+ **Embedding process**:
110
+
111
+ ```python
112
+ from sentence_transformers import SentenceTransformer
113
+
114
+ model = SentenceTransformer("BAAI/bge-m3")
115
+ query_vector = model.encode(
116
+ query,
117
+ normalize_embeddings=True # L2 normalization for cosine similarity
118
+ )
119
+ # Returns: np.array of shape (1024,)
120
+ ```
121
+
122
+ ### Catalog Embedding
123
+
124
+ Software tools are pre-embedded during indexing:
125
+
126
+ ```python
127
+ # For each tool in catalog:
128
+ tool_text = f"{tool.name} {tool.description} {' '.join(tool.keywords)}"
129
+ tool_vector = model.encode(tool_text, normalize_embeddings=True)
130
+
131
+ # Store in FAISS index
132
+ faiss_index.add(tool_vector)
133
+ ```
134
+
135
+ **Index structure**:
136
+
137
+ - FAISS IndexFlatIP (inner product = cosine similarity for normalized vectors)
138
+ - ~150 tools in current catalog
139
+ - Index size: ~600KB
140
+
141
+ ## Step 4: FAISS Search
142
+
143
+ ### Vector Search
144
+
145
+ FAISS performs fast similarity search:
146
+
147
+ ```python
148
+ import faiss
149
+
150
+ # Search for top 20 most similar tools
151
+ scores, indices = faiss_index.search(
152
+ query_vector.reshape(1, -1),
153
+ k=20
154
+ )
155
+
156
+ # Returns:
157
+ # scores: [0.85, 0.82, 0.79, ...] # Cosine similarities
158
+ # indices: [42, 17, 89, ...] # Tool IDs in catalog
159
+ ```
160
+
161
+ **Search algorithm**:
162
+
163
+ - IndexFlatIP: Exact search (brute force)
164
+ - Fast for catalog size (~150 tools)
165
+ - Could use IVF for larger catalogs (>10k tools)
166
+
167
+ **Why top-20**:
168
+
169
+ - More candidates than needed (default final: 8)
170
+ - Provides options for reranking
171
+ - Balances recall vs. later stage cost
172
+
173
+ ### Candidate Retrieval
174
+
175
+ ```python
176
+ candidates = [catalog[idx] for idx in indices[:20]]
177
+ candidate_scores = scores[:20].tolist()
178
+
179
+ # Example candidates:
180
+ [
181
+ {
182
+ "name": "TotalSegmentator",
183
+ "score": 0.85,
184
+ "description": "Automated multi-organ segmentation...",
185
+ ...
186
+ },
187
+ ...
188
+ ]
189
+ ```
190
+
191
+ ## Step 5: CrossEncoder Reranking
192
+
193
+ ### Why Rerank?
194
+
195
+ **BiEncoder (BGE-M3)** limitations:
196
+
197
+ - Encodes query and documents independently
198
+ - No query-document interaction
199
+ - Misses subtle relevance signals
200
+
201
+ **CrossEncoder** benefits:
202
+
203
+ - Jointly encodes query + document
204
+ - Cross-attention between query and doc
205
+ - More accurate relevance scoring
206
+ - Slower (not suitable for entire catalog)
207
+
208
+ ### Reranking Model
209
+
210
+ **Model**: `BAAI/bge-reranker-v2-m3`
211
+
212
+ **Characteristics**:
213
+
214
+ - Trained on MS-MARCO passage ranking
215
+ - 6 layers, fast inference (~50ms per pair)
216
+ - Direct relevance score (no embedding)
217
+
218
+ **Reranking process**:
219
+
220
+ ```python
221
+ from sentence_transformers import CrossEncoder
222
+
223
+ reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")
224
+
225
+ # Score each (query, candidate) pair
226
+ pairs = [(query, candidate.description) for candidate in candidates]
227
+ rerank_scores = reranker.predict(pairs)
228
+
229
+ # Re-sort by rerank scores
230
+ sorted_indices = np.argsort(rerank_scores)[::-1]
231
+ reranked_candidates = [candidates[i] for i in sorted_indices][:8]
232
+ ```
233
+
234
+ **Output**: Top-8 candidates with refined ranking
235
+
236
+ ## Output Format
237
+
238
+ ### Candidate Schema
239
+
240
+ Each candidate passed to Stage 2:
241
+
242
+ ```python
243
+ {
244
+ "name": "TotalSegmentator",
245
+ "description": "Automated multi-organ segmentation for CT and MRI",
246
+ "url": "https://github.com/wasserth/TotalSegmentator",
247
+ "keywords": ["segmentation", "medical-imaging", "CT", "MRI"],
248
+ "license": "Apache-2.0",
249
+ "supporting_data": {
250
+ "modalities": ["CT", "MRI"],
251
+ "dimensions": ["3D"],
252
+ "formats": ["DICOM", "NIfTI"],
253
+ "demo_url": "https://huggingface.co/spaces/..."
254
+ },
255
+ "retrieval_score": 0.85, # FAISS or rerank score
256
+ }
257
+ ```
258
+
259
+ **Fields used by VLM**:
260
+
261
+ - Essential for understanding tool capability
262
+ - Formatted as table in VLM prompt
263
+ - Enables comparative reasoning
264
+
265
+ ## Index Management
266
+
267
+ ### Building the Index
268
+
269
+ Done during catalog sync:
270
+
271
+ ```bash
272
+ ai_agent sync
273
+ ```
274
+
275
+ **Process**:
276
+
277
+ 1. Load catalog JSONL
278
+ 2. Embed each tool description
279
+ 3. Build FAISS index
280
+ 4. Save to disk: `artifacts/rag_index/`
281
+
282
+ **Files**:
283
+ ```
284
+ artifacts/rag_index/
285
+ ├── index.faiss # FAISS binary index
286
+ └── meta.json # Metadata (tool IDs, config)
287
+ ```
288
+
289
+ ### Loading the Index
290
+
291
+ At startup:
292
+
293
+ ```python
294
+ from retriever.vector_index import VectorIndex
295
+
296
+ index = VectorIndex()
297
+ index.load("artifacts/rag_index")
298
+
299
+ # Ready for queries
300
+ results = index.search(query, k=20)
301
+ ```
302
+
303
+ ### Updating the Index
304
+
305
+ When catalog changes:
306
+ 1. Sync detects new/modified tools
307
+ 2. Re-embed entire catalog (fast, ~2 seconds)
308
+ 3. Rebuild FAISS index
309
+ 4. Reload in pipeline (no restart needed)
310
+
311
+ ## Performance Optimization
312
+
313
+ ### Caching
314
+
315
+ **Model loading**:
316
+
317
+ - BGE-M3 and CrossEncoder loaded once at startup
318
+ - Kept in memory for entire session
319
+
320
+ **Index loading**:
321
+
322
+ - FAISS index loaded once
323
+ - Small enough to fit in memory (~MB)
324
+
325
+ ### Batch Processing
326
+
327
+ For multiple queries (testing, batch mode):
328
+
329
+ ```python
330
+ # Batch embed multiple queries
331
+ query_vectors = model.encode(queries, batch_size=32)
332
+
333
+ # Batch FAISS search
334
+ scores, indices = index.search(query_vectors, k=20)
335
+ ```
336
+
337
+ ### GPU Acceleration
338
+
339
+ Models can use GPU if available:
340
+
341
+ ```python
342
+ model = SentenceTransformer("BAAI/bge-m3", device="cuda")
343
+ reranker = CrossEncoder("...", device="cuda")
344
+ ```
345
+
346
+ ## Retrieval Metrics
347
+
348
+ ### Monitored Metrics
349
+
350
+ During retrieval:
351
+
352
+ - **Number of candidates found**: Should be ≥8 for good coverage
353
+ - **Average similarity score**: Higher = better match
354
+ - **Reranking impact**: Score change after reranking
355
+ - **Retry usage**: Whether broadening retry was triggered
356
+
357
+ ### Logging
358
+
359
+ Retrieval events logged:
360
+
361
+ ```
362
+ INFO retriever.vector_index: FAISS search: query="segment lungs", results=20, top_score=0.85
363
+ INFO retriever.reranker: Reranking 20 candidates, top_score_change=+0.12
364
+ INFO retriever.pipeline: Final candidates: 8, avg_score=0.78
365
+ ```
366
+
367
+ ## Limitations
368
+
369
+ ### Current Limitations
370
+
371
+ 1. **English only**: No multilingual support (though model is capable)
372
+ 2. **Small catalog**: ~150 tools (FAISS overkill, but scales)
373
+ 3. **No filtering**: Can't filter by license, modality in retrieval (done in Stage 2)
374
+ 4. **Heuristic retries**: Broadening strategy is simple prefix-based shortening
375
+
376
+ ### Future Enhancements
377
+
378
+ - **Hybrid search**: Combine semantic + keyword (BM25)
379
+ - **Metadata filters**: Pre-filter by modality, license, format
380
+ - **Personalization**: User history, preferences
381
+ - **Adaptive retries**: Learn better broadening formulations from query logs
382
+
383
+ ## Next Steps
384
+
385
+ - Learn about [Agent & VLM Selection](agent.md)
386
+ - Explore [Software Catalog](catalog.md)
387
+ - Return to [Architecture Overview](overview.md)
docs/development/contributing.md CHANGED
@@ -1,315 +1,315 @@
1
- # Contributing
2
-
3
- Thank you for your interest in contributing to the AI Imaging Agent! This guide will help you get started.
4
-
5
- ## Getting Started
6
-
7
- ### 1. Fork and Clone
8
-
9
- ```bash
10
- # Fork the repository on GitHub
11
- # Then clone your fork
12
- git clone https://github.com/YOUR_USERNAME/ai-agent.git
13
- cd ai-agent
14
- ```
15
-
16
- ### 2. Set Up Development Environment
17
-
18
- ```bash
19
- # Create virtual environment
20
- python -m venv .venv
21
- source .venv/bin/activate # On Windows: .venv\Scripts\activate
22
-
23
- # Install in development mode
24
- pip install -e ".[dev]"
25
- ```
26
-
27
- ### 3. Create a Branch
28
-
29
- ```bash
30
- git checkout -b feature/your-feature-name
31
- # or
32
- git checkout -b fix/issue-description
33
- ```
34
-
35
- ## Development Workflow
36
-
37
- ### Making Changes
38
-
39
- 1. **Make your changes** in the appropriate module
40
- 2. **Test your changes** (see [Testing](testing.md))
41
- 3. **Update documentation** if needed
42
- 4. **Update CHANGELOG.md** following [Keep a Changelog](https://keepachangelog.com/) format
43
-
44
- <!-- ### Code Style
45
-
46
- We use standard Python tools for code quality:
47
-
48
- #### Black
49
-
50
- Code formatting:
51
-
52
- ```bash
53
- black src/ tests/
54
- ```
55
-
56
- #### Ruff
57
-
58
- Linting:
59
-
60
- ```bash
61
- ruff check src/ tests/
62
- ``` -->
63
-
64
- #### Type Checking
65
-
66
- MyPy for type checking:
67
-
68
- ```bash
69
- mypy src/
70
- ```
71
-
72
- ### Running Tests
73
-
74
- ```bash
75
- # Run all tests
76
- pytest tests/
77
-
78
- # Run specific test file
79
- pytest tests/test_retrieval_pipeline.py
80
-
81
- # Run with coverage
82
- pytest --cov=ai_agent tests/
83
- ```
84
-
85
- ## Contribution Guidelines
86
-
87
- ### Code Quality
88
-
89
- - **Follow PEP 8**: Use Black for formatting
90
- - **Type hints**: Add type annotations to new functions
91
- - **Docstrings**: Document all public functions and classes
92
- - **Tests**: Add tests for new functionality
93
- - **No warnings**: Fix any linter warnings before submitting
94
-
95
- ### Commit Messages
96
-
97
- Use clear, descriptive commit messages:
98
-
99
- ```
100
- feat: Add alternative search tool for agent
101
-
102
- - Implement search_alternative tool
103
- - Add retry logic for insufficient results
104
- - Update agent prompt with tool description
105
- ```
106
-
107
- **Format**:
108
-
109
- - `feat:` New features
110
- - `fix:` Bug fixes
111
- - `docs:` Documentation changes
112
- - `test:` Test additions/changes
113
- - `refactor:` Code refactoring
114
- - `chore:` Maintenance tasks
115
-
116
- ### Pull Requests
117
-
118
- 1. **Update CHANGELOG.md** under `[Unreleased]` section
119
- 2. **Write clear PR description** explaining changes
120
- 3. **Link related issues** if applicable
121
- 4. **Request review** from maintainers
122
-
123
- **PR description template**:
124
-
125
- ```markdown
126
- ## Description
127
- Brief description of changes
128
-
129
- ## Type of Change
130
- - [ ] Bug fix
131
- - [ ] New feature
132
- - [ ] Documentation update
133
- - [ ] Refactoring
134
-
135
- ## Testing
136
- How to test these changes
137
-
138
- ## Checklist
139
- - [ ] Code follows project style
140
- - [ ] Tests added/updated
141
- - [ ] Documentation updated
142
- - [ ] CHANGELOG.md updated
143
- ```
144
-
145
- ## Areas to Contribute
146
-
147
- ### High Priority
148
-
149
- - **Catalog expansion**: Add more imaging tools
150
- - **Demo integration**: Improve Gradio Space execution and add new spaces for tools
151
- - **Format support**: Add new image format handlers
152
-
153
- <!-- ### Good First Issues
154
-
155
- Look for issues tagged `good-first-issue` on GitHub:
156
-
157
- - Bug fixes
158
- - Documentation improvements
159
- - Test coverage expansion
160
- - Example scripts -->
161
-
162
- ### Feature Requests
163
-
164
- Before implementing new features:
165
-
166
- 1. **Check existing issues** for similar requests
167
- 2. **Open an issue** to discuss the feature
168
- 3. **Get feedback** from maintainers
169
- 4. **Implement** after discussion
170
-
171
- ## Documentation
172
-
173
- ### Writing Documentation
174
-
175
- Documentation lives in `docs/` and uses MkDocs Material.
176
-
177
- ```bash
178
- # Install MkDocs
179
- pip install mkdocs-material
180
-
181
- # Serve locally
182
- mkdocs serve
183
-
184
- # Open http://127.0.0.1:8000
185
- ```
186
-
187
- ### Documentation Style
188
-
189
- - Use **clear headings** and structure
190
- - Include **code examples** where relevant
191
- - Add **warnings** and **tips** for important information
192
- - Keep **language simple** and accessible
193
-
194
- ## Adding Tools to Catalog
195
-
196
- ### Process
197
-
198
- 1. **Create tool entry** in `dataset/catalog.jsonl`:
199
-
200
- ```json
201
- {
202
- "@type": "SoftwareSourceCode",
203
- "name": "ToolName",
204
- "description": "Tool description",
205
- "url": "https://github.com/user/tool",
206
- "license": "Apache-2.0",
207
- "keywords": ["segmentation", "CT"],
208
- "supportingData": {
209
- "modalities": ["CT"],
210
- "dimensions": ["3D"],
211
- "formats": ["DICOM"],
212
- "demo_url": "https://huggingface.co/spaces/user/tool"
213
- }
214
- }
215
- ```
216
-
217
- 2. **Validate entry**:
218
-
219
- ```bash
220
- # Check JSON syntax
221
- python -c "import json; print(json.loads('YOUR_JSON_HERE'))"
222
- ```
223
-
224
- 3. **Update checksum**:
225
-
226
- ```bash
227
- shasum dataset/catalog.jsonl > dataset/catalog.jsonl.sha1
228
- ```
229
-
230
- 4. **Sync catalog**:
231
-
232
- ```bash
233
- ai_agent sync
234
- ```
235
-
236
- 5. **Test retrieval**:
237
-
238
- ```bash
239
- ai_agent chat
240
- # Try queries that should return your new tool
241
- ```
242
-
243
- ### Tool Criteria
244
-
245
- Tools should:
246
-
247
- - ✅ Be relevant to imaging analysis
248
- - ✅ Be actively maintained
249
- - ✅ Have clear documentation
250
- - ✅ Preferably have a runnable demo
251
- - ✅ Be open-source or have free tier
252
-
253
- ## Reporting Issues
254
-
255
- ### Bug Reports
256
-
257
- Include:
258
-
259
- - **Description** of the bug
260
- - **Steps to reproduce**
261
- - **Expected behavior**
262
- - **Actual behavior**
263
- - **Environment** (OS, Python version, etc.)
264
- - **Logs** if available
265
-
266
- ### Feature Requests
267
-
268
- Include:
269
-
270
- - **Use case** for the feature
271
- - **Proposed solution** (if you have one)
272
- - **Alternatives considered**
273
- - **Examples** of similar features
274
-
275
- ## Code Review Process
276
-
277
- 1. **Automated checks** run on PR (tests, linting)
278
- 2. **Maintainer review** provides feedback
279
- 3. **Address feedback** and update PR
280
- 4. **Approval** from maintainer(s)
281
- 5. **Merge** into main branch
282
-
283
- ## Release Process
284
-
285
- Releases follow semantic versioning:
286
-
287
- 1. Update version in `pyproject.toml`
288
- 2. Update `CHANGELOG.md` with release date
289
- 3. Create git tag: `git tag v0.2.0`
290
- 4. Push tag: `git push origin v0.2.0`
291
- 5. GitHub Actions deploys documentation
292
-
293
- ## Getting Help
294
-
295
- - **GitHub Discussions**: Ask questions
296
- - **GitHub Issues**: Report bugs
297
-
298
- ## Code of Conduct
299
-
300
- Be respectful and professional:
301
-
302
- - Use welcoming and inclusive language
303
- - Respect differing viewpoints
304
- - Accept constructive criticism
305
- - Focus on what's best for the community
306
-
307
- ## License
308
-
309
- By contributing, you agree that your contributions will be licensed under the Apache 2.0 License.
310
-
311
- ## Next Steps
312
-
313
- - Review [Project Structure](structure.md)
314
- - Learn about [Testing](testing.md)
315
- - Read [Architecture Overview](../architecture/overview.md)
 
1
+ # Contributing
2
+
3
+ Thank you for your interest in contributing to the AI Imaging Agent! This guide will help you get started.
4
+
5
+ ## Getting Started
6
+
7
+ ### 1. Fork and Clone
8
+
9
+ ```bash
10
+ # Fork the repository on GitHub
11
+ # Then clone your fork
12
+ git clone https://github.com/YOUR_USERNAME/ai-agent.git
13
+ cd ai-agent
14
+ ```
15
+
16
+ ### 2. Set Up Development Environment
17
+
18
+ ```bash
19
+ # Create virtual environment
20
+ python -m venv .venv
21
+ source .venv/bin/activate # On Windows: .venv\Scripts\activate
22
+
23
+ # Install in development mode
24
+ pip install -e ".[dev]"
25
+ ```
26
+
27
+ ### 3. Create a Branch
28
+
29
+ ```bash
30
+ git checkout -b feature/your-feature-name
31
+ # or
32
+ git checkout -b fix/issue-description
33
+ ```
34
+
35
+ ## Development Workflow
36
+
37
+ ### Making Changes
38
+
39
+ 1. **Make your changes** in the appropriate module
40
+ 2. **Test your changes** (see [Testing](testing.md))
41
+ 3. **Update documentation** if needed
42
+ 4. **Update CHANGELOG.md** following [Keep a Changelog](https://keepachangelog.com/) format
43
+
44
+ <!-- ### Code Style
45
+
46
+ We use standard Python tools for code quality:
47
+
48
+ #### Black
49
+
50
+ Code formatting:
51
+
52
+ ```bash
53
+ black src/ tests/
54
+ ```
55
+
56
+ #### Ruff
57
+
58
+ Linting:
59
+
60
+ ```bash
61
+ ruff check src/ tests/
62
+ ``` -->
63
+
64
+ #### Type Checking
65
+
66
+ MyPy for type checking:
67
+
68
+ ```bash
69
+ mypy src/
70
+ ```
71
+
72
+ ### Running Tests
73
+
74
+ ```bash
75
+ # Run all tests
76
+ pytest tests/
77
+
78
+ # Run specific test file
79
+ pytest tests/test_retrieval_pipeline.py
80
+
81
+ # Run with coverage
82
+ pytest --cov=ai_agent tests/
83
+ ```
84
+
85
+ ## Contribution Guidelines
86
+
87
+ ### Code Quality
88
+
89
+ - **Follow PEP 8**: Use Black for formatting
90
+ - **Type hints**: Add type annotations to new functions
91
+ - **Docstrings**: Document all public functions and classes
92
+ - **Tests**: Add tests for new functionality
93
+ - **No warnings**: Fix any linter warnings before submitting
94
+
95
+ ### Commit Messages
96
+
97
+ Use clear, descriptive commit messages:
98
+
99
+ ```
100
+ feat: Add alternative search tool for agent
101
+
102
+ - Implement search_alternative tool
103
+ - Add retry logic for insufficient results
104
+ - Update agent prompt with tool description
105
+ ```
106
+
107
+ **Format**:
108
+
109
+ - `feat:` New features
110
+ - `fix:` Bug fixes
111
+ - `docs:` Documentation changes
112
+ - `test:` Test additions/changes
113
+ - `refactor:` Code refactoring
114
+ - `chore:` Maintenance tasks
115
+
116
+ ### Pull Requests
117
+
118
+ 1. **Update CHANGELOG.md** under `[Unreleased]` section
119
+ 2. **Write clear PR description** explaining changes
120
+ 3. **Link related issues** if applicable
121
+ 4. **Request review** from maintainers
122
+
123
+ **PR description template**:
124
+
125
+ ```markdown
126
+ ## Description
127
+ Brief description of changes
128
+
129
+ ## Type of Change
130
+ - [ ] Bug fix
131
+ - [ ] New feature
132
+ - [ ] Documentation update
133
+ - [ ] Refactoring
134
+
135
+ ## Testing
136
+ How to test these changes
137
+
138
+ ## Checklist
139
+ - [ ] Code follows project style
140
+ - [ ] Tests added/updated
141
+ - [ ] Documentation updated
142
+ - [ ] CHANGELOG.md updated
143
+ ```
144
+
145
+ ## Areas to Contribute
146
+
147
+ ### High Priority
148
+
149
+ - **Catalog expansion**: Add more imaging tools
150
+ - **Demo integration**: Improve Gradio Space execution and add new spaces for tools
151
+ - **Format support**: Add new image format handlers
152
+
153
+ <!-- ### Good First Issues
154
+
155
+ Look for issues tagged `good-first-issue` on GitHub:
156
+
157
+ - Bug fixes
158
+ - Documentation improvements
159
+ - Test coverage expansion
160
+ - Example scripts -->
161
+
162
+ ### Feature Requests
163
+
164
+ Before implementing new features:
165
+
166
+ 1. **Check existing issues** for similar requests
167
+ 2. **Open an issue** to discuss the feature
168
+ 3. **Get feedback** from maintainers
169
+ 4. **Implement** after discussion
170
+
171
+ ## Documentation
172
+
173
+ ### Writing Documentation
174
+
175
+ Documentation lives in `docs/` and uses MkDocs Material.
176
+
177
+ ```bash
178
+ # Install MkDocs
179
+ pip install mkdocs-material
180
+
181
+ # Serve locally
182
+ mkdocs serve
183
+
184
+ # Open http://127.0.0.1:8000
185
+ ```
186
+
187
+ ### Documentation Style
188
+
189
+ - Use **clear headings** and structure
190
+ - Include **code examples** where relevant
191
+ - Add **warnings** and **tips** for important information
192
+ - Keep **language simple** and accessible
193
+
194
+ ## Adding Tools to Catalog
195
+
196
+ ### Process
197
+
198
+ 1. **Create tool entry** in `dataset/catalog.jsonl`:
199
+
200
+ ```json
201
+ {
202
+ "@type": "SoftwareSourceCode",
203
+ "name": "ToolName",
204
+ "description": "Tool description",
205
+ "url": "https://github.com/user/tool",
206
+ "license": "Apache-2.0",
207
+ "keywords": ["segmentation", "CT"],
208
+ "supportingData": {
209
+ "modalities": ["CT"],
210
+ "dimensions": ["3D"],
211
+ "formats": ["DICOM"],
212
+ "demo_url": "https://huggingface.co/spaces/user/tool"
213
+ }
214
+ }
215
+ ```
216
+
217
+ 2. **Validate entry**:
218
+
219
+ ```bash
220
+ # Check JSON syntax
221
+ python -c "import json; print(json.loads('YOUR_JSON_HERE'))"
222
+ ```
223
+
224
+ 3. **Update checksum**:
225
+
226
+ ```bash
227
+ shasum dataset/catalog.jsonl > dataset/catalog.jsonl.sha1
228
+ ```
229
+
230
+ 4. **Sync catalog**:
231
+
232
+ ```bash
233
+ ai_agent sync
234
+ ```
235
+
236
+ 5. **Test retrieval**:
237
+
238
+ ```bash
239
+ ai_agent chat
240
+ # Try queries that should return your new tool
241
+ ```
242
+
243
+ ### Tool Criteria
244
+
245
+ Tools should:
246
+
247
+ - ✅ Be relevant to imaging analysis
248
+ - ✅ Be actively maintained
249
+ - ✅ Have clear documentation
250
+ - ✅ Preferably have a runnable demo
251
+ - ✅ Be open-source or have free tier
252
+
253
+ ## Reporting Issues
254
+
255
+ ### Bug Reports
256
+
257
+ Include:
258
+
259
+ - **Description** of the bug
260
+ - **Steps to reproduce**
261
+ - **Expected behavior**
262
+ - **Actual behavior**
263
+ - **Environment** (OS, Python version, etc.)
264
+ - **Logs** if available
265
+
266
+ ### Feature Requests
267
+
268
+ Include:
269
+
270
+ - **Use case** for the feature
271
+ - **Proposed solution** (if you have one)
272
+ - **Alternatives considered**
273
+ - **Examples** of similar features
274
+
275
+ ## Code Review Process
276
+
277
+ 1. **Automated checks** run on PR (tests, linting)
278
+ 2. **Maintainer review** provides feedback
279
+ 3. **Address feedback** and update PR
280
+ 4. **Approval** from maintainer(s)
281
+ 5. **Merge** into main branch
282
+
283
+ ## Release Process
284
+
285
+ Releases follow semantic versioning:
286
+
287
+ 1. Update version in `pyproject.toml`
288
+ 2. Update `CHANGELOG.md` with release date
289
+ 3. Create git tag: `git tag v0.2.0`
290
+ 4. Push tag: `git push origin v0.2.0`
291
+ 5. GitHub Actions deploys documentation
292
+
293
+ ## Getting Help
294
+
295
+ - **GitHub Discussions**: Ask questions
296
+ - **GitHub Issues**: Report bugs
297
+
298
+ ## Code of Conduct
299
+
300
+ Be respectful and professional:
301
+
302
+ - Use welcoming and inclusive language
303
+ - Respect differing viewpoints
304
+ - Accept constructive criticism
305
+ - Focus on what's best for the community
306
+
307
+ ## License
308
+
309
+ By contributing, you agree that your contributions will be licensed under the Apache 2.0 License.
310
+
311
+ ## Next Steps
312
+
313
+ - Review [Project Structure](structure.md)
314
+ - Learn about [Testing](testing.md)
315
+ - Read [Architecture Overview](../architecture/overview.md)
docs/development/structure.md CHANGED
@@ -1,362 +1,362 @@
1
- # Project Structure
2
-
3
- The AI Imaging Agent is organized into modular components with clear separation of concerns.
4
-
5
- ## Directory Layout
6
-
7
- ```
8
- ai-agent/
9
- ├── .github/
10
- │ └── workflows/ # CI/CD workflows
11
- │ └── deploy_docs.yml # Documentation deployment
12
- ├── artifacts/
13
- │ └── rag_index/ # FAISS index and embeddings
14
- ├── dataset/
15
- │ └── catalog.jsonl # Software catalog
16
- ├── docs/ # MkDocs documentation
17
- ├── logs/ # Application logs
18
- ├── src/
19
- │ └── ai_agent/ # Main package
20
- │ ├── agent/ # PydanticAI agent
21
- │ ├── api/ # Pipeline orchestration
22
- │ ├── catalog/ # Catalog management
23
- │ ├── generator/ # VLM selection (schemas)
24
- │ ├── retriever/ # Text retrieval
25
- │ ├── ui/ # Gradio interface
26
- │ └── utils/ # Shared utilities
27
- ├── tests/ # Test suite
28
- ├── config.yaml # Model configuration
29
- ├── mkdocs.yml # Documentation config
30
- ├── pyproject.toml # Package metadata
31
- └── README.md # Project readme
32
- ```
33
-
34
- ## Core Modules
35
-
36
- ### src/ai_agent/
37
-
38
- Main package containing all application code.
39
-
40
- #### agent/
41
-
42
- PydanticAI conversational agent implementation.
43
-
44
- ```
45
- agent/
46
- ├── __init__.py
47
- ├── agent.py # Agent definition, tool adapters
48
- ├── models.py # Agent output/log models
49
- ├── utils.py # Agent state and tool quota helpers
50
- └── tools/ # Tool implementations (search, repo_info, mcp)
51
- ```
52
-
53
- **Key components**:
54
-
55
- - `agent.py`: Agent instance, system prompt, tool definitions
56
- - `models.py`: Agent output and tool usage schemas
57
- - `utils.py`: `AgentState` plus call caps/prepare hooks
58
- - `tools/`: Tool implementations (search, alternatives, repo info, mcp tools)
59
-
60
- **Dependencies**: `api/`, `utils/`
61
-
62
- #### api/
63
-
64
- Pipeline orchestration and core logic.
65
-
66
- ```
67
- api/
68
- ├── __init__.py
69
- └── pipeline.py # RAGImagingPipeline main class
70
- ```
71
-
72
- **Responsibilities**:
73
-
74
- - File validation and metadata extraction
75
- - Retrieval + VLM selection orchestration
76
- - Error handling and logging
77
- - Index management
78
-
79
- **Dependencies**: `retriever/`, `generator/`, `utils/`
80
-
81
- #### catalog/
82
-
83
- Software catalog synchronization.
84
-
85
- ```
86
- catalog/
87
- ├── __init__.py
88
- └── sync.py # Catalog sync logic
89
- ```
90
-
91
- **Functions**:
92
-
93
- - Load catalog from JSONL
94
- - Check for changes (SHA1)
95
- - Trigger index rebuild
96
-
97
- **Dependencies**: `retriever/`
98
-
99
- #### generator/
100
-
101
- VLM selection schemas and types.
102
-
103
- ```
104
- generator/
105
- ├── __init__.py
106
- └── schema.py # Pydantic models for responses
107
- ```
108
-
109
- **Models**:
110
-
111
- - `ToolRecommendation`: Individual tool recommendation
112
- - `AgentResponse`: Complete response with status
113
- - `ConversationStatus`: Enum for conversation states
114
- - `ToolReason`: Enum for recommendation reasons
115
-
116
- **Dependencies**: None (pure schemas)
117
-
118
- #### retriever/
119
-
120
- Text-based retrieval pipeline.
121
-
122
- ```
123
- retriever/
124
- ├── __init__.py
125
- ├── text_embedder.py # BGE-M3 embedding model
126
- ├── vector_index.py # FAISS index management
127
- ├── reranker.py # CrossEncoder reranking
128
- └── software_doc.py # Catalog schema and loading
129
- ```
130
-
131
- **Pipeline flow**:
132
-
133
- 1. `text_embedder.py`: Embed query
134
- 2. `vector_index.py`: FAISS search
135
- 3. `reranker.py`: CrossEncoder reranking
136
- 4. Output: Top-K candidates
137
-
138
- **Dependencies**: None (pure retrieval)
139
-
140
- #### ui/
141
-
142
- Gradio web interface.
143
-
144
- ```
145
- ui/
146
- ├── __init__.py
147
- ├── app.py # Gradio app definition
148
- ├── components.py # Reusable UI components
149
- ├── formatters.py # Response formatting
150
- ├── handlers.py # Message handlers
151
- ├── state.py # UI state management
152
- └── visualizations.py # Preview and trace rendering
153
- ```
154
-
155
- **Key files**:
156
-
157
- - `app.py`: Main Gradio interface
158
- - `handlers.py`: `respond()` function - core interaction logic
159
- - `formatters.py`: Format recommendations as markdown/cards
160
- - `components.py`: Reusable Gradio components
161
-
162
- **Dependencies**: `agent/`, `api/`
163
-
164
- #### utils/
165
-
166
- Shared utilities.
167
-
168
- ```
169
- utils/
170
- ├── __init__.py
171
- ├── config.py # Configuration loading
172
- ├── file_validator.py # File validation
173
- ├── image_meta.py # Metadata extraction (DICOM, NIfTI, TIFF)
174
- ├── previews.py # Image preview generation
175
- └── tags.py # Control tag parsing
176
- ```
177
-
178
- **Common utilities**:
179
-
180
- - `config.py`: Load `config.yaml` with Pydantic validation
181
- - `file_validator.py`: Size limits, format checks
182
- - `image_meta.py`: Extract DICOM/NIfTI/TIFF metadata
183
- - `previews.py`: Convert medical images to PNG
184
- - `tags.py`: Parse exclusion tags and strip control tags from queries
185
-
186
- **Dependencies**: None (pure utilities)
187
-
188
- #### cli.py
189
-
190
- Command-line interface entry point.
191
-
192
- ```python
193
- def main():
194
- # Parse arguments
195
- # Route to chat or sync
196
- ```
197
-
198
- **Commands**:
199
-
200
- - `ai_agent chat`: Launch UI
201
- - `ai_agent sync`: Sync catalog
202
-
203
- ### tests/
204
-
205
- Test suite.
206
-
207
- ```
208
- tests/
209
- ├── data/
210
- │ └── test_data.json # Test cases
211
- ├── test_retrieval_pipeline.py
212
- ├── test_deepwiki_repo_info.py
213
- └── ...
214
- ```
215
-
216
- **Test categories**:
217
-
218
- - Unit tests: Individual components
219
- - Integration tests: Full pipeline
220
- - End-to-end tests: Real API calls (optional)
221
-
222
- ## Configuration Files
223
-
224
- ### pyproject.toml
225
-
226
- Python package metadata and dependencies.
227
-
228
- ```toml
229
- [project]
230
- name = "ai_agent"
231
- version = "1.0.0"
232
- dependencies = [...]
233
-
234
- [project.scripts]
235
- ai_agent = "ai_agent.cli:main"
236
- ```
237
-
238
- ### config.yaml
239
-
240
- Model configuration.
241
-
242
- ```yaml
243
- agent_model:
244
- name: "gpt-4o-mini"
245
- base_url: null
246
- api_key_env: "OPENAI_API_KEY"
247
-
248
- available_models:
249
- - display_name: "gpt-4o-mini"
250
- name: "gpt-4o-mini"
251
- ...
252
- ```
253
-
254
- ### mkdocs.yml
255
-
256
- Documentation configuration.
257
-
258
- ```yaml
259
- site_name: AI Imaging Agent
260
- theme:
261
- name: material
262
- nav: [...]
263
- ```
264
-
265
- ### .env
266
-
267
- Environment variables (not committed).
268
-
269
- ```dotenv
270
- OPENAI_API_KEY=sk-xxxx
271
- SOFTWARE_CATALOG=dataset/catalog.jsonl
272
- ```
273
-
274
- ## Data Files
275
-
276
- ### dataset/catalog.jsonl
277
-
278
- Software catalog in JSON Lines format.
279
-
280
- Each line is a complete JSON object following schema.org SoftwareSourceCode.
281
-
282
- ### artifacts/rag_index/
283
-
284
- Pre-built FAISS index and metadata.
285
-
286
- ```
287
- artifacts/rag_index/
288
- ├── index.faiss # FAISS binary index
289
- └── meta.json # Tool IDs, config, timestamps
290
- ```
291
-
292
- ## Module Boundaries
293
-
294
- Clear separation prevents circular dependencies:
295
-
296
- ```
297
- ui/ → agent/ → api/ → retriever/
298
- → generator/
299
- → utils/
300
- ```
301
-
302
- **Rules**:
303
-
304
- - `utils/`: No dependencies on other modules
305
- - `retriever/`: Pure retrieval, no generation
306
- - `generator/`: Pure schemas, no retrieval
307
- - `api/`: Orchestrates retriever + generator
308
- - `agent/`: Uses api for tool calls
309
- - `ui/`: Top-level, depends on agent + api
310
-
311
- ## Import Patterns
312
-
313
- All imports use absolute paths from `ai_agent`:
314
-
315
- ```python
316
- from ai_agent.retriever.vector_index import VectorIndex
317
- from ai_agent.utils.config import load_config
318
- from ai_agent.agent.utils import AgentState
319
- ```
320
-
321
- **Never use** relative imports like `from ..utils import ...`
322
-
323
- ## Extension Points
324
-
325
- ### Adding New Tools
326
-
327
- Add tool adapters to `agent/agent.py` and implement logic in `agent/tools/`:
328
-
329
- ```python
330
- @agent.tool
331
- async def new_tool(ctx: RunContext[AgentState], param: str) -> str:
332
- """Tool description."""
333
- # Implementation
334
- return result
335
- ```
336
-
337
- ### Adding New Metadata Extractors
338
-
339
- Add to `utils/image_meta.py`:
340
-
341
- ```python
342
- def extract_custom_format(file_path: str) -> dict:
343
- """Extract metadata from custom format."""
344
- # Implementation
345
- return metadata
346
- ```
347
-
348
- ### Adding New Retrieval Models
349
-
350
- Replace in `retriever/text_embedder.py`:
351
-
352
- ```python
353
- class TextEmbedder:
354
- def __init__(self, model_name="new-embedding-model"):
355
- self.model = SentenceTransformer(model_name)
356
- ```
357
-
358
- ## Next Steps
359
-
360
- - Learn about [Contributing](contributing.md)
361
- - Explore [Testing](testing.md)
362
- - Return to [Architecture Overview](../architecture/overview.md)
 
1
+ # Project Structure
2
+
3
+ The AI Imaging Agent is organized into modular components with clear separation of concerns.
4
+
5
+ ## Directory Layout
6
+
7
+ ```
8
+ ai-agent/
9
+ ├── .github/
10
+ │ └── workflows/ # CI/CD workflows
11
+ │ └── deploy_docs.yml # Documentation deployment
12
+ ├── artifacts/
13
+ │ └── rag_index/ # FAISS index and embeddings
14
+ ├── dataset/
15
+ │ └── catalog.jsonl # Software catalog
16
+ ├── docs/ # MkDocs documentation
17
+ ├── logs/ # Application logs
18
+ ├── src/
19
+ │ └── ai_agent/ # Main package
20
+ │ ├── agent/ # PydanticAI agent
21
+ │ ├── api/ # Pipeline orchestration
22
+ │ ├── catalog/ # Catalog management
23
+ │ ├── generator/ # VLM selection (schemas)
24
+ │ ├── retriever/ # Text retrieval
25
+ │ ├── ui/ # Gradio interface
26
+ │ └── utils/ # Shared utilities
27
+ ├── tests/ # Test suite
28
+ ├── config.yaml # Model configuration
29
+ ├── mkdocs.yml # Documentation config
30
+ ├── pyproject.toml # Package metadata
31
+ └── README.md # Project readme
32
+ ```
33
+
34
+ ## Core Modules
35
+
36
+ ### src/ai_agent/
37
+
38
+ Main package containing all application code.
39
+
40
+ #### agent/
41
+
42
+ PydanticAI conversational agent implementation.
43
+
44
+ ```
45
+ agent/
46
+ ├── __init__.py
47
+ ├── agent.py # Agent definition, tool adapters
48
+ ├── models.py # Agent output/log models
49
+ ├── utils.py # Agent state and tool quota helpers
50
+ └── tools/ # Tool implementations (search, repo_info, mcp)
51
+ ```
52
+
53
+ **Key components**:
54
+
55
+ - `agent.py`: Agent instance, system prompt, tool definitions
56
+ - `models.py`: Agent output and tool usage schemas
57
+ - `utils.py`: `AgentState` plus call caps/prepare hooks
58
+ - `tools/`: Tool implementations (search, alternatives, repo info, mcp tools)
59
+
60
+ **Dependencies**: `api/`, `utils/`
61
+
62
+ #### api/
63
+
64
+ Pipeline orchestration and core logic.
65
+
66
+ ```
67
+ api/
68
+ ├── __init__.py
69
+ └── pipeline.py # RAGImagingPipeline main class
70
+ ```
71
+
72
+ **Responsibilities**:
73
+
74
+ - File validation and metadata extraction
75
+ - Retrieval + VLM selection orchestration
76
+ - Error handling and logging
77
+ - Index management
78
+
79
+ **Dependencies**: `retriever/`, `generator/`, `utils/`
80
+
81
+ #### catalog/
82
+
83
+ Software catalog synchronization.
84
+
85
+ ```
86
+ catalog/
87
+ ├── __init__.py
88
+ └── sync.py # Catalog sync logic
89
+ ```
90
+
91
+ **Functions**:
92
+
93
+ - Load catalog from JSONL
94
+ - Check for changes (SHA1)
95
+ - Trigger index rebuild
96
+
97
+ **Dependencies**: `retriever/`
98
+
99
+ #### generator/
100
+
101
+ VLM selection schemas and types.
102
+
103
+ ```
104
+ generator/
105
+ ├── __init__.py
106
+ └── schema.py # Pydantic models for responses
107
+ ```
108
+
109
+ **Models**:
110
+
111
+ - `ToolRecommendation`: Individual tool recommendation
112
+ - `AgentResponse`: Complete response with status
113
+ - `ConversationStatus`: Enum for conversation states
114
+ - `ToolReason`: Enum for recommendation reasons
115
+
116
+ **Dependencies**: None (pure schemas)
117
+
118
+ #### retriever/
119
+
120
+ Text-based retrieval pipeline.
121
+
122
+ ```
123
+ retriever/
124
+ ├── __init__.py
125
+ ├── text_embedder.py # BGE-M3 embedding model
126
+ ├── vector_index.py # FAISS index management
127
+ ├── reranker.py # CrossEncoder reranking
128
+ └── software_doc.py # Catalog schema and loading
129
+ ```
130
+
131
+ **Pipeline flow**:
132
+
133
+ 1. `text_embedder.py`: Embed query
134
+ 2. `vector_index.py`: FAISS search
135
+ 3. `reranker.py`: CrossEncoder reranking
136
+ 4. Output: Top-K candidates
137
+
138
+ **Dependencies**: None (pure retrieval)
139
+
140
+ #### ui/
141
+
142
+ Gradio web interface.
143
+
144
+ ```
145
+ ui/
146
+ ├── __init__.py
147
+ ├── app.py # Gradio app definition
148
+ ├── components.py # Reusable UI components
149
+ ├── formatters.py # Response formatting
150
+ ├── handlers.py # Message handlers
151
+ ├── state.py # UI state management
152
+ └── visualizations.py # Preview and trace rendering
153
+ ```
154
+
155
+ **Key files**:
156
+
157
+ - `app.py`: Main Gradio interface
158
+ - `handlers.py`: `respond()` function - core interaction logic
159
+ - `formatters.py`: Format recommendations as markdown/cards
160
+ - `components.py`: Reusable Gradio components
161
+
162
+ **Dependencies**: `agent/`, `api/`
163
+
164
+ #### utils/
165
+
166
+ Shared utilities.
167
+
168
+ ```
169
+ utils/
170
+ ├── __init__.py
171
+ ├── config.py # Configuration loading
172
+ ├── file_validator.py # File validation
173
+ ├── image_meta.py # Metadata extraction (DICOM, NIfTI, TIFF)
174
+ ├── previews.py # Image preview generation
175
+ └── tags.py # Control tag parsing
176
+ ```
177
+
178
+ **Common utilities**:
179
+
180
+ - `config.py`: Load `config.yaml` with Pydantic validation
181
+ - `file_validator.py`: Size limits, format checks
182
+ - `image_meta.py`: Extract DICOM/NIfTI/TIFF metadata
183
+ - `previews.py`: Convert medical images to PNG
184
+ - `tags.py`: Parse exclusion tags and strip control tags from queries
185
+
186
+ **Dependencies**: None (pure utilities)
187
+
188
+ #### cli.py
189
+
190
+ Command-line interface entry point.
191
+
192
+ ```python
193
+ def main():
194
+ # Parse arguments
195
+ # Route to chat or sync
196
+ ```
197
+
198
+ **Commands**:
199
+
200
+ - `ai_agent chat`: Launch UI
201
+ - `ai_agent sync`: Sync catalog
202
+
203
+ ### tests/
204
+
205
+ Test suite.
206
+
207
+ ```
208
+ tests/
209
+ ├── data/
210
+ │ └── test_data.json # Test cases
211
+ ├── test_retrieval_pipeline.py
212
+ ├── test_deepwiki_repo_info.py
213
+ └── ...
214
+ ```
215
+
216
+ **Test categories**:
217
+
218
+ - Unit tests: Individual components
219
+ - Integration tests: Full pipeline
220
+ - End-to-end tests: Real API calls (optional)
221
+
222
+ ## Configuration Files
223
+
224
+ ### pyproject.toml
225
+
226
+ Python package metadata and dependencies.
227
+
228
+ ```toml
229
+ [project]
230
+ name = "ai_agent"
231
+ version = "1.0.0"
232
+ dependencies = [...]
233
+
234
+ [project.scripts]
235
+ ai_agent = "ai_agent.cli:main"
236
+ ```
237
+
238
+ ### config.yaml
239
+
240
+ Model configuration.
241
+
242
+ ```yaml
243
+ agent_model:
244
+ name: "gpt-4o-mini"
245
+ base_url: null
246
+ api_key_env: "OPENAI_API_KEY"
247
+
248
+ available_models:
249
+ - display_name: "gpt-4o-mini"
250
+ name: "gpt-4o-mini"
251
+ ...
252
+ ```
253
+
254
+ ### mkdocs.yml
255
+
256
+ Documentation configuration.
257
+
258
+ ```yaml
259
+ site_name: AI Imaging Agent
260
+ theme:
261
+ name: material
262
+ nav: [...]
263
+ ```
264
+
265
+ ### .env
266
+
267
+ Environment variables (not committed).
268
+
269
+ ```dotenv
270
+ OPENAI_API_KEY=sk-xxxx
271
+ SOFTWARE_CATALOG=dataset/catalog.jsonl
272
+ ```
273
+
274
+ ## Data Files
275
+
276
+ ### dataset/catalog.jsonl
277
+
278
+ Software catalog in JSON Lines format.
279
+
280
+ Each line is a complete JSON object following schema.org SoftwareSourceCode.
281
+
282
+ ### artifacts/rag_index/
283
+
284
+ Pre-built FAISS index and metadata.
285
+
286
+ ```
287
+ artifacts/rag_index/
288
+ ├── index.faiss # FAISS binary index
289
+ └── meta.json # Tool IDs, config, timestamps
290
+ ```
291
+
292
+ ## Module Boundaries
293
+
294
+ Clear separation prevents circular dependencies:
295
+
296
+ ```
297
+ ui/ → agent/ → api/ → retriever/
298
+ → generator/
299
+ → utils/
300
+ ```
301
+
302
+ **Rules**:
303
+
304
+ - `utils/`: No dependencies on other modules
305
+ - `retriever/`: Pure retrieval, no generation
306
+ - `generator/`: Pure schemas, no retrieval
307
+ - `api/`: Orchestrates retriever + generator
308
+ - `agent/`: Uses api for tool calls
309
+ - `ui/`: Top-level, depends on agent + api
310
+
311
+ ## Import Patterns
312
+
313
+ All imports use absolute paths from `ai_agent`:
314
+
315
+ ```python
316
+ from ai_agent.retriever.vector_index import VectorIndex
317
+ from ai_agent.utils.config import load_config
318
+ from ai_agent.agent.utils import AgentState
319
+ ```
320
+
321
+ **Never use** relative imports like `from ..utils import ...`
322
+
323
+ ## Extension Points
324
+
325
+ ### Adding New Tools
326
+
327
+ Add tool adapters to `agent/agent.py` and implement logic in `agent/tools/`:
328
+
329
+ ```python
330
+ @agent.tool
331
+ async def new_tool(ctx: RunContext[AgentState], param: str) -> str:
332
+ """Tool description."""
333
+ # Implementation
334
+ return result
335
+ ```
336
+
337
+ ### Adding New Metadata Extractors
338
+
339
+ Add to `utils/image_meta.py`:
340
+
341
+ ```python
342
+ def extract_custom_format(file_path: str) -> dict:
343
+ """Extract metadata from custom format."""
344
+ # Implementation
345
+ return metadata
346
+ ```
347
+
348
+ ### Adding New Retrieval Models
349
+
350
+ Replace in `retriever/text_embedder.py`:
351
+
352
+ ```python
353
+ class TextEmbedder:
354
+ def __init__(self, model_name="new-embedding-model"):
355
+ self.model = SentenceTransformer(model_name)
356
+ ```
357
+
358
+ ## Next Steps
359
+
360
+ - Learn about [Contributing](contributing.md)
361
+ - Explore [Testing](testing.md)
362
+ - Return to [Architecture Overview](../architecture/overview.md)
docs/development/testing.md CHANGED
@@ -1,410 +1,410 @@
1
- # Testing (still under development)
2
-
3
- The AI Imaging Agent uses pytest for testing. This guide covers running tests and writing new ones.
4
-
5
- **Note:** We are still developing some tests for the agent, hence this part is not relevant for now.
6
-
7
- ## Running Tests
8
-
9
- ### Basic Usage
10
-
11
- ```bash
12
- # Run all tests
13
- pytest
14
-
15
- # Run specific test file
16
- pytest tests/test_retrieval_pipeline.py
17
-
18
- # Run specific test
19
- pytest tests/test_retrieval_pipeline.py::test_basic_retrieval
20
-
21
- # Run with verbose output
22
- pytest -v
23
-
24
- # Run with coverage
25
- pytest --cov=ai_agent --cov-report=html
26
- ```
27
-
28
- ### Test Categories
29
-
30
- Tests are marked by category:
31
-
32
- ```bash
33
- # Run only unit tests
34
- pytest -m unit
35
-
36
- # Run only integration tests
37
- pytest -m integration
38
-
39
- # Skip slow tests
40
- pytest -m "not slow"
41
- ```
42
-
43
- ## Test Organization
44
-
45
- ### Directory Structure
46
-
47
- ```
48
- tests/
49
- ├── data/
50
- │ ├── test_data.json # Test cases
51
- │ └── 0002.DCM # Sample DICOM file
52
- ├── test_retrieval_pipeline.py # Retrieval tests
53
- ├── test_deepwiki_repo_info.py # Repo info tests
54
- ├── test_gpt4o_vision.py # VLM tests (integration)
55
- └── __pycache__/
56
- ```
57
-
58
- ### Test File Naming
59
-
60
- - `test_*.py`: Test files
61
- - `*_test.py`: Alternative naming (less common)
62
-
63
- ### Test Function Naming
64
-
65
- ```python
66
- def test_basic_retrieval():
67
- """Test basic retrieval functionality."""
68
- pass
69
-
70
- def test_edge_case_empty_query():
71
- """Test handling of empty query."""
72
- pass
73
-
74
- def test_integration_full_pipeline():
75
- """Integration test for complete pipeline."""
76
- pass
77
- ```
78
-
79
- ## Writing Tests
80
-
81
- ### Unit Test Example
82
-
83
- ```python
84
- import pytest
85
- from ai_agent.retriever.vector_index import VectorIndex
86
-
87
- def test_vector_index_search():
88
- """Test FAISS vector search."""
89
- # Arrange
90
- index = VectorIndex()
91
- index.load("artifacts/rag_index")
92
-
93
- query = "segment lungs CT"
94
-
95
- # Act
96
- results = index.search(query, k=5)
97
-
98
- # Assert
99
- assert len(results) == 5
100
- assert all(r['score'] > 0 for r in results)
101
- assert 'TotalSegmentator' in [r['name'] for r in results]
102
- ```
103
-
104
- ### Integration Test Example
105
-
106
- ```python
107
- import pytest
108
- from ai_agent.api.pipeline import RAGImagingPipeline
109
-
110
- @pytest.mark.integration
111
- def test_full_pipeline_with_image():
112
- """Integration test with real image and VLM call."""
113
- # Arrange
114
- pipeline = RAGImagingPipeline(
115
- catalog_path="dataset/catalog.jsonl",
116
- index_dir="artifacts/rag_index"
117
- )
118
-
119
- # Act
120
- result = pipeline.recommend(
121
- query="segment lungs",
122
- files=["tests/data/chest_ct.dcm"]
123
- )
124
-
125
- # Assert
126
- assert result.status == "complete"
127
- assert len(result.recommendations) > 0
128
- assert result.recommendations[0].accuracy_score > 70
129
- ```
130
-
131
- ### Parametrized Tests
132
-
133
- ```python
134
- @pytest.mark.parametrize("query,expected_tool", [
135
- ("segment brain MRI", "FreeSurfer"),
136
- ("segment lungs CT", "TotalSegmentator"),
137
- ("classify chest X-ray", "CheXNet"),
138
- ])
139
- def test_retrieval_for_queries(query, expected_tool):
140
- """Test retrieval returns expected tools for various queries."""
141
- index = VectorIndex()
142
- index.load("artifacts/rag_index")
143
-
144
- results = index.search(query, k=10)
145
- tool_names = [r['name'] for r in results]
146
-
147
- assert expected_tool in tool_names
148
- ```
149
-
150
- ### Fixtures
151
-
152
- ```python
153
- import pytest
154
-
155
- @pytest.fixture
156
- def pipeline():
157
- """Provide initialized pipeline for tests."""
158
- return RAGImagingPipeline(
159
- catalog_path="dataset/catalog.jsonl",
160
- index_dir="artifacts/rag_index"
161
- )
162
-
163
- @pytest.fixture
164
- def sample_dicom():
165
- """Provide path to sample DICOM file."""
166
- return "tests/data/0002.DCM"
167
-
168
- def test_with_fixtures(pipeline, sample_dicom):
169
- """Test using fixtures."""
170
- result = pipeline.recommend(
171
- query="analyze DICOM",
172
- files=[sample_dicom]
173
- )
174
- assert result is not None
175
- ```
176
-
177
- <!-- ## Mocking
178
-
179
- ### Mocking VLM Calls
180
-
181
- To avoid API costs during testing:
182
-
183
- ```python
184
- from unittest.mock import Mock, patch
185
- import pytest
186
-
187
- @pytest.fixture
188
- def mock_vlm_response():
189
- """Mock VLM response."""
190
- return {
191
- "status": "complete",
192
- "recommendations": [
193
- {
194
- "rank": 1,
195
- "name": "TotalSegmentator",
196
- "accuracy_score": 95,
197
- "explanation": "Test explanation",
198
- "reason": "task_match"
199
- }
200
- ]
201
- }
202
-
203
- def test_with_mocked_vlm(mock_vlm_response):
204
- """Test pipeline with mocked VLM."""
205
- with patch('ai_agent.agent.agent.Agent.run') as mock_run:
206
- mock_run.return_value = mock_vlm_response
207
-
208
- # Test code here
209
- result = pipeline.recommend(query="test", files=[])
210
-
211
- assert result["status"] == "complete"
212
- ```
213
-
214
- ### Mocking File Operations
215
-
216
- ```python
217
- def test_file_validation():
218
- """Test file validation without real files."""
219
- with patch('os.path.getsize') as mock_size:
220
- mock_size.return_value = 1024 * 1024 # 1 MB
221
-
222
- from ai_agent.utils.file_validator import validate_file
223
- is_valid = validate_file("fake.dcm")
224
-
225
- assert is_valid
226
- ``` -->
227
-
228
- ## Test Data
229
-
230
- ### Using Test Cases
231
-
232
- Load test cases from JSON:
233
-
234
- ```python
235
- import json
236
-
237
- def load_test_cases():
238
- """Load test cases from data file."""
239
- with open("tests/data/test_data.json") as f:
240
- return json.load(f)
241
-
242
- @pytest.mark.parametrize("test_case", load_test_cases())
243
- def test_from_json(test_case):
244
- """Test using cases from JSON file."""
245
- query = test_case["query"]
246
- expected = test_case["expected_tool"]
247
-
248
- # Test logic here
249
- assert expected in results
250
- ```
251
-
252
- ### Sample Data Files
253
-
254
- Keep sample files small:
255
-
256
- - **DICOM**: Single slice, low resolution
257
- - **NIfTI**: Small volume (e.g., 64×64×64)
258
- - **Images**: PNG/JPG under 1 MB
259
-
260
- <!-- ## Coverage
261
-
262
- ### Measuring Coverage
263
-
264
- ```bash
265
- # Run with coverage
266
- pytest --cov=ai_agent
267
-
268
- # Generate HTML report
269
- pytest --cov=ai_agent --cov-report=html
270
-
271
- # Open report
272
- open htmlcov/index.html # macOS
273
- # or
274
- xdg-open htmlcov/index.html # Linux
275
- ```
276
-
277
- ### Coverage Goals
278
-
279
- Aim for:
280
-
281
- - **Overall**: >80%
282
- - **Critical paths**: >90% (retrieval, agent, pipeline)
283
- - **Utilities**: >70%
284
-
285
- ### Coverage Configuration
286
-
287
- In `pyproject.toml`:
288
-
289
- ```toml
290
- [tool.coverage.run]
291
- source = ["src/ai_agent"]
292
- omit = ["tests/*", "*/migrations/*"]
293
-
294
- [tool.coverage.report]
295
- precision = 2
296
- show_missing = true
297
- skip_covered = false
298
- ``` -->
299
-
300
- ## Continuous Integration
301
-
302
- ### GitHub Actions
303
-
304
- Tests run automatically on:
305
-
306
- - Pull requests
307
- - Pushes to main
308
-
309
- ### CI Configuration
310
-
311
- ```yaml
312
- # .github/workflows/test.yml
313
- name: Tests
314
-
315
- on: [push, pull_request]
316
-
317
- jobs:
318
- test:
319
- runs-on: ubuntu-latest
320
- steps:
321
- - uses: actions/checkout@v4
322
- - uses: actions/setup-python@v5
323
- with:
324
- python-version: '3.10'
325
- - run: pip install -e ".[dev]"
326
- - run: pytest --cov=ai_agent
327
- ```
328
-
329
- ## Best Practices
330
-
331
- ### Do's
332
-
333
- ✅ **Test edge cases**: Empty inputs, invalid data, etc.
334
- ✅ **Test error handling**: Verify exceptions are caught
335
- ✅ **Use descriptive names**: `test_retrieval_with_empty_query` not `test1`
336
- ✅ **Keep tests isolated**: Each test should be independent
337
- ✅ **Use fixtures**: Avoid repeating setup code
338
- ✅ **Mock expensive operations**: VLM calls, network requests
339
-
340
- ### Don'ts
341
-
342
- ❌ **Don't test implementation details**: Test behavior, not internal state
343
- ❌ **Don't make tests depend on each other**: Each should run independently
344
- ❌ **Don't commit large test files**: Keep test data small
345
- ❌ **Don't skip error checking**: Test both success and failure paths
346
-
347
- ## Performance Testing
348
-
349
- ### Benchmarking
350
-
351
- Use pytest-benchmark:
352
-
353
- ```python
354
- def test_retrieval_performance(benchmark):
355
- """Benchmark retrieval speed."""
356
- index = VectorIndex()
357
- index.load("artifacts/rag_index")
358
-
359
- result = benchmark(index.search, "segment lungs", k=10)
360
-
361
- assert len(result) == 10
362
- ```
363
-
364
- ### Profiling
365
-
366
- ```bash
367
- # Profile tests
368
- pytest --profile
369
-
370
- # Generate SVG profile
371
- pytest --profile-svg
372
- ```
373
-
374
- ## Debugging Tests
375
-
376
- ### Running in Debug Mode
377
-
378
- ```python
379
- # Add to test
380
- import pdb; pdb.set_trace()
381
-
382
- # Run pytest
383
- pytest tests/test_file.py
384
- ```
385
-
386
- ### Verbose Output
387
-
388
- ```bash
389
- # Show print statements
390
- pytest -s
391
-
392
- # Very verbose
393
- pytest -vv
394
-
395
- # Show local variables on failure
396
- pytest -l
397
- ```
398
-
399
- ### Running Single Test
400
-
401
- ```bash
402
- # Run one test function
403
- pytest tests/test_file.py::test_function_name -v
404
- ```
405
-
406
- ## Next Steps
407
-
408
- - Review [Project Structure](structure.md)
409
- - Read [Contributing Guide](contributing.md)
410
- - Explore [Architecture](../architecture/overview.md)
 
1
+ # Testing (still under development)
2
+
3
+ The AI Imaging Agent uses pytest for testing. This guide covers running tests and writing new ones.
4
+
5
+ **Note:** We are still developing some tests for the agent, hence this part is not relevant for now.
6
+
7
+ ## Running Tests
8
+
9
+ ### Basic Usage
10
+
11
+ ```bash
12
+ # Run all tests
13
+ pytest
14
+
15
+ # Run specific test file
16
+ pytest tests/test_retrieval_pipeline.py
17
+
18
+ # Run specific test
19
+ pytest tests/test_retrieval_pipeline.py::test_basic_retrieval
20
+
21
+ # Run with verbose output
22
+ pytest -v
23
+
24
+ # Run with coverage
25
+ pytest --cov=ai_agent --cov-report=html
26
+ ```
27
+
28
+ ### Test Categories
29
+
30
+ Tests are marked by category:
31
+
32
+ ```bash
33
+ # Run only unit tests
34
+ pytest -m unit
35
+
36
+ # Run only integration tests
37
+ pytest -m integration
38
+
39
+ # Skip slow tests
40
+ pytest -m "not slow"
41
+ ```
42
+
43
+ ## Test Organization
44
+
45
+ ### Directory Structure
46
+
47
+ ```
48
+ tests/
49
+ ├── data/
50
+ │ ├── test_data.json # Test cases
51
+ │ └── 0002.DCM # Sample DICOM file
52
+ ├── test_retrieval_pipeline.py # Retrieval tests
53
+ ├── test_deepwiki_repo_info.py # Repo info tests
54
+ ├── test_gpt4o_vision.py # VLM tests (integration)
55
+ └── __pycache__/
56
+ ```
57
+
58
+ ### Test File Naming
59
+
60
+ - `test_*.py`: Test files
61
+ - `*_test.py`: Alternative naming (less common)
62
+
63
+ ### Test Function Naming
64
+
65
+ ```python
66
+ def test_basic_retrieval():
67
+ """Test basic retrieval functionality."""
68
+ pass
69
+
70
+ def test_edge_case_empty_query():
71
+ """Test handling of empty query."""
72
+ pass
73
+
74
+ def test_integration_full_pipeline():
75
+ """Integration test for complete pipeline."""
76
+ pass
77
+ ```
78
+
79
+ ## Writing Tests
80
+
81
+ ### Unit Test Example
82
+
83
+ ```python
84
+ import pytest
85
+ from ai_agent.retriever.vector_index import VectorIndex
86
+
87
+ def test_vector_index_search():
88
+ """Test FAISS vector search."""
89
+ # Arrange
90
+ index = VectorIndex()
91
+ index.load("artifacts/rag_index")
92
+
93
+ query = "segment lungs CT"
94
+
95
+ # Act
96
+ results = index.search(query, k=5)
97
+
98
+ # Assert
99
+ assert len(results) == 5
100
+ assert all(r['score'] > 0 for r in results)
101
+ assert 'TotalSegmentator' in [r['name'] for r in results]
102
+ ```
103
+
104
+ ### Integration Test Example
105
+
106
+ ```python
107
+ import pytest
108
+ from ai_agent.api.pipeline import RAGImagingPipeline
109
+
110
+ @pytest.mark.integration
111
+ def test_full_pipeline_with_image():
112
+ """Integration test with real image and VLM call."""
113
+ # Arrange
114
+ pipeline = RAGImagingPipeline(
115
+ catalog_path="dataset/catalog.jsonl",
116
+ index_dir="artifacts/rag_index"
117
+ )
118
+
119
+ # Act
120
+ result = pipeline.recommend(
121
+ query="segment lungs",
122
+ files=["tests/data/chest_ct.dcm"]
123
+ )
124
+
125
+ # Assert
126
+ assert result.status == "complete"
127
+ assert len(result.recommendations) > 0
128
+ assert result.recommendations[0].accuracy_score > 70
129
+ ```
130
+
131
+ ### Parametrized Tests
132
+
133
+ ```python
134
+ @pytest.mark.parametrize("query,expected_tool", [
135
+ ("segment brain MRI", "FreeSurfer"),
136
+ ("segment lungs CT", "TotalSegmentator"),
137
+ ("classify chest X-ray", "CheXNet"),
138
+ ])
139
+ def test_retrieval_for_queries(query, expected_tool):
140
+ """Test retrieval returns expected tools for various queries."""
141
+ index = VectorIndex()
142
+ index.load("artifacts/rag_index")
143
+
144
+ results = index.search(query, k=10)
145
+ tool_names = [r['name'] for r in results]
146
+
147
+ assert expected_tool in tool_names
148
+ ```
149
+
150
+ ### Fixtures
151
+
152
+ ```python
153
+ import pytest
154
+
155
+ @pytest.fixture
156
+ def pipeline():
157
+ """Provide initialized pipeline for tests."""
158
+ return RAGImagingPipeline(
159
+ catalog_path="dataset/catalog.jsonl",
160
+ index_dir="artifacts/rag_index"
161
+ )
162
+
163
+ @pytest.fixture
164
+ def sample_dicom():
165
+ """Provide path to sample DICOM file."""
166
+ return "tests/data/0002.DCM"
167
+
168
+ def test_with_fixtures(pipeline, sample_dicom):
169
+ """Test using fixtures."""
170
+ result = pipeline.recommend(
171
+ query="analyze DICOM",
172
+ files=[sample_dicom]
173
+ )
174
+ assert result is not None
175
+ ```
176
+
177
+ <!-- ## Mocking
178
+
179
+ ### Mocking VLM Calls
180
+
181
+ To avoid API costs during testing:
182
+
183
+ ```python
184
+ from unittest.mock import Mock, patch
185
+ import pytest
186
+
187
+ @pytest.fixture
188
+ def mock_vlm_response():
189
+ """Mock VLM response."""
190
+ return {
191
+ "status": "complete",
192
+ "recommendations": [
193
+ {
194
+ "rank": 1,
195
+ "name": "TotalSegmentator",
196
+ "accuracy_score": 95,
197
+ "explanation": "Test explanation",
198
+ "reason": "task_match"
199
+ }
200
+ ]
201
+ }
202
+
203
+ def test_with_mocked_vlm(mock_vlm_response):
204
+ """Test pipeline with mocked VLM."""
205
+ with patch('ai_agent.agent.agent.Agent.run') as mock_run:
206
+ mock_run.return_value = mock_vlm_response
207
+
208
+ # Test code here
209
+ result = pipeline.recommend(query="test", files=[])
210
+
211
+ assert result["status"] == "complete"
212
+ ```
213
+
214
+ ### Mocking File Operations
215
+
216
+ ```python
217
+ def test_file_validation():
218
+ """Test file validation without real files."""
219
+ with patch('os.path.getsize') as mock_size:
220
+ mock_size.return_value = 1024 * 1024 # 1 MB
221
+
222
+ from ai_agent.utils.file_validator import validate_file
223
+ is_valid = validate_file("fake.dcm")
224
+
225
+ assert is_valid
226
+ ``` -->
227
+
228
+ ## Test Data
229
+
230
+ ### Using Test Cases
231
+
232
+ Load test cases from JSON:
233
+
234
+ ```python
235
+ import json
236
+
237
+ def load_test_cases():
238
+ """Load test cases from data file."""
239
+ with open("tests/data/test_data.json") as f:
240
+ return json.load(f)
241
+
242
+ @pytest.mark.parametrize("test_case", load_test_cases())
243
+ def test_from_json(test_case):
244
+ """Test using cases from JSON file."""
245
+ query = test_case["query"]
246
+ expected = test_case["expected_tool"]
247
+
248
+ # Test logic here
249
+ assert expected in results
250
+ ```
251
+
252
+ ### Sample Data Files
253
+
254
+ Keep sample files small:
255
+
256
+ - **DICOM**: Single slice, low resolution
257
+ - **NIfTI**: Small volume (e.g., 64×64×64)
258
+ - **Images**: PNG/JPG under 1 MB
259
+
260
+ <!-- ## Coverage
261
+
262
+ ### Measuring Coverage
263
+
264
+ ```bash
265
+ # Run with coverage
266
+ pytest --cov=ai_agent
267
+
268
+ # Generate HTML report
269
+ pytest --cov=ai_agent --cov-report=html
270
+
271
+ # Open report
272
+ open htmlcov/index.html # macOS
273
+ # or
274
+ xdg-open htmlcov/index.html # Linux
275
+ ```
276
+
277
+ ### Coverage Goals
278
+
279
+ Aim for:
280
+
281
+ - **Overall**: >80%
282
+ - **Critical paths**: >90% (retrieval, agent, pipeline)
283
+ - **Utilities**: >70%
284
+
285
+ ### Coverage Configuration
286
+
287
+ In `pyproject.toml`:
288
+
289
+ ```toml
290
+ [tool.coverage.run]
291
+ source = ["src/ai_agent"]
292
+ omit = ["tests/*", "*/migrations/*"]
293
+
294
+ [tool.coverage.report]
295
+ precision = 2
296
+ show_missing = true
297
+ skip_covered = false
298
+ ``` -->
299
+
300
+ ## Continuous Integration
301
+
302
+ ### GitHub Actions
303
+
304
+ Tests run automatically on:
305
+
306
+ - Pull requests
307
+ - Pushes to main
308
+
309
+ ### CI Configuration
310
+
311
+ ```yaml
312
+ # .github/workflows/test.yml
313
+ name: Tests
314
+
315
+ on: [push, pull_request]
316
+
317
+ jobs:
318
+ test:
319
+ runs-on: ubuntu-latest
320
+ steps:
321
+ - uses: actions/checkout@v4
322
+ - uses: actions/setup-python@v5
323
+ with:
324
+ python-version: '3.10'
325
+ - run: pip install -e ".[dev]"
326
+ - run: pytest --cov=ai_agent
327
+ ```
328
+
329
+ ## Best Practices
330
+
331
+ ### Do's
332
+
333
+ ✅ **Test edge cases**: Empty inputs, invalid data, etc.
334
+ ✅ **Test error handling**: Verify exceptions are caught
335
+ ✅ **Use descriptive names**: `test_retrieval_with_empty_query` not `test1`
336
+ ✅ **Keep tests isolated**: Each test should be independent
337
+ ✅ **Use fixtures**: Avoid repeating setup code
338
+ ✅ **Mock expensive operations**: VLM calls, network requests
339
+
340
+ ### Don'ts
341
+
342
+ ❌ **Don't test implementation details**: Test behavior, not internal state
343
+ ❌ **Don't make tests depend on each other**: Each should run independently
344
+ ❌ **Don't commit large test files**: Keep test data small
345
+ ❌ **Don't skip error checking**: Test both success and failure paths
346
+
347
+ ## Performance Testing
348
+
349
+ ### Benchmarking
350
+
351
+ Use pytest-benchmark:
352
+
353
+ ```python
354
+ def test_retrieval_performance(benchmark):
355
+ """Benchmark retrieval speed."""
356
+ index = VectorIndex()
357
+ index.load("artifacts/rag_index")
358
+
359
+ result = benchmark(index.search, "segment lungs", k=10)
360
+
361
+ assert len(result) == 10
362
+ ```
363
+
364
+ ### Profiling
365
+
366
+ ```bash
367
+ # Profile tests
368
+ pytest --profile
369
+
370
+ # Generate SVG profile
371
+ pytest --profile-svg
372
+ ```
373
+
374
+ ## Debugging Tests
375
+
376
+ ### Running in Debug Mode
377
+
378
+ ```python
379
+ # Add to test
380
+ import pdb; pdb.set_trace()
381
+
382
+ # Run pytest
383
+ pytest tests/test_file.py
384
+ ```
385
+
386
+ ### Verbose Output
387
+
388
+ ```bash
389
+ # Show print statements
390
+ pytest -s
391
+
392
+ # Very verbose
393
+ pytest -vv
394
+
395
+ # Show local variables on failure
396
+ pytest -l
397
+ ```
398
+
399
+ ### Running Single Test
400
+
401
+ ```bash
402
+ # Run one test function
403
+ pytest tests/test_file.py::test_function_name -v
404
+ ```
405
+
406
+ ## Next Steps
407
+
408
+ - Review [Project Structure](structure.md)
409
+ - Read [Contributing Guide](contributing.md)
410
+ - Explore [Architecture](../architecture/overview.md)
docs/getting-started/configuration.md CHANGED
@@ -1,165 +1,165 @@
1
- # Configuration
2
-
3
- Before running the AI Imaging Agent, you need to configure it with your API keys and preferences.
4
-
5
- ## Environment Variables
6
-
7
- Create a `.env` file in the repository root with the following configuration:
8
-
9
- ```dotenv
10
- # Required: OpenAI API key
11
- OPENAI_API_KEY=sk-xxxx
12
-
13
- # Optional: GitHub token for repository info tool
14
- GITHUB_TOKEN=ghp_xxxx
15
-
16
- # Optional: Alternative model providers
17
- EPFL_API_KEY=sk-xxxx
18
-
19
- # Software catalog path
20
- SOFTWARE_CATALOG=dataset/catalog.jsonl
21
-
22
- # Logging configuration
23
- LOGLEVEL_CONSOLE=WARNING
24
- LOGLEVEL_FILE=INFO
25
- FILE_LOG=1
26
- LOG_DIR=logs
27
- LOG_PROMPTS=0 # Set to 1 to save prompt snapshots for debugging
28
-
29
- # Custom config path
30
- CONFIG_PATH=config.yaml
31
- ```
32
-
33
- ## Required Configuration
34
-
35
- ### OpenAI API Key
36
-
37
- The AI Imaging Agent requires an OpenAI API key for the vision-language model:
38
-
39
- 1. Sign up for an account at [OpenAI](https://platform.openai.com/)
40
- 2. Navigate to [API Keys](https://platform.openai.com/api-keys)
41
- 3. Create a new API key
42
- 4. Add it to your `.env` file:
43
-
44
- ```dotenv
45
- OPENAI_API_KEY=sk-your-actual-key-here
46
- ```
47
-
48
- ## Model Configuration
49
-
50
- The agent model can be configured via `config.yaml`:
51
-
52
- ```yaml
53
- # AI Agent Model Configuration
54
-
55
- # Default/fallback model (used for CLI and initial startup)
56
- agent_model:
57
- name: "gpt-4o-mini"
58
- base_url: null # null for default OpenAI endpoint
59
- api_key_env: "OPENAI_API_KEY"
60
-
61
- # Available models for UI dropdown
62
- available_models:
63
- - display_name: "gpt-4o-mini"
64
- name: "gpt-4o-mini"
65
- base_url: null
66
- provider: "OpenAI"
67
- api_key_env: "OPENAI_API_KEY"
68
-
69
- - display_name: "gpt-4o"
70
- name: "gpt-4o"
71
- base_url: null
72
- provider: "OpenAI"
73
- api_key_env: "OPENAI_API_KEY"
74
-
75
- - display_name: "gpt-5.1"
76
- name: "gpt-5.1"
77
- base_url: null
78
- provider: "OpenAI"
79
- api_key_env: "OPENAI_API_KEY"
80
- ```
81
-
82
- ### Using Alternative Model Providers
83
-
84
- You can configure custom OpenAI-compatible endpoints:
85
-
86
- ```yaml
87
- available_models:
88
- - display_name: "EPFL Inference"
89
- name: "gpt-4o-mini"
90
- base_url: "https://inference.epfl.ch/v1"
91
- provider: "EPFL"
92
- api_key_env: "EPFL_API_KEY"
93
- ```
94
-
95
- Then add the corresponding API key to your `.env`:
96
-
97
- ```dotenv
98
- EPFL_API_KEY=your-epfl-key
99
- ```
100
-
101
- ## Optional Configuration
102
-
103
- ### GitHub Token
104
-
105
- For the repository info tool (optional):
106
-
107
- ```dotenv
108
- GITHUB_TOKEN=ghp_your_github_personal_access_token
109
- ```
110
-
111
- This enables the agent to fetch detailed information about GitHub repositories.
112
-
113
- ### Pipeline Parameters
114
-
115
- Adjust retrieval and recommendation settings directly in the app setting. You can change the `TOP_K` and `NUM_CHOICES` parameters.
116
-
117
- ### Logging
118
-
119
- Configure logging behavior:
120
-
121
- ```dotenv
122
- # Console log level (DEBUG, INFO, WARNING, ERROR)
123
- LOGLEVEL_CONSOLE=WARNING
124
-
125
- # File log level
126
- LOGLEVEL_FILE=INFO
127
-
128
- # Enable file logging (0 or 1)
129
- FILE_LOG=1
130
-
131
- # Log directory
132
- LOG_DIR=logs
133
-
134
- # Save VLM prompts and images for debugging (0 or 1)
135
- LOG_PROMPTS=0
136
- ```
137
-
138
- !!! tip "Debug Mode"
139
- Set `LOG_PROMPTS=1` to save VLM prompts and images to the `logs/` directory. This is useful for debugging but will increase disk usage.
140
-
141
- ### Software Catalog
142
-
143
- Specify the path to your software catalog:
144
-
145
- ```dotenv
146
- SOFTWARE_CATALOG=dataset/catalog.jsonl
147
- ```
148
-
149
- The catalog should be in JSONL format following the schema.org SoftwareSourceCode structure.
150
-
151
- ## Verification
152
-
153
- After configuring, verify your setup:
154
-
155
- ```bash
156
- # Check that environment variables are loaded
157
- python -c "from dotenv import load_dotenv; import os; load_dotenv(); print('API Key:', 'SET' if os.getenv('OPENAI_API_KEY') else 'NOT SET')"
158
- ```
159
-
160
- ## Next Steps
161
-
162
- With configuration complete, you're ready to:
163
-
164
- - [Run the Quick Start](quickstart.md)
165
- - Learn about [Using the Chat Interface](../user-guide/chat-interface.md)
 
1
+ # Configuration
2
+
3
+ Before running the AI Imaging Agent, you need to configure it with your API keys and preferences.
4
+
5
+ ## Environment Variables
6
+
7
+ Create a `.env` file in the repository root with the following configuration:
8
+
9
+ ```dotenv
10
+ # Required: OpenAI API key
11
+ OPENAI_API_KEY=sk-xxxx
12
+
13
+ # Optional: GitHub token for repository info tool
14
+ GITHUB_TOKEN=ghp_xxxx
15
+
16
+ # Optional: Alternative model providers
17
+ EPFL_API_KEY=sk-xxxx
18
+
19
+ # Software catalog path
20
+ SOFTWARE_CATALOG=dataset/catalog.jsonl
21
+
22
+ # Logging configuration
23
+ LOGLEVEL_CONSOLE=WARNING
24
+ LOGLEVEL_FILE=INFO
25
+ FILE_LOG=1
26
+ LOG_DIR=logs
27
+ LOG_PROMPTS=0 # Set to 1 to save prompt snapshots for debugging
28
+
29
+ # Custom config path
30
+ CONFIG_PATH=config.yaml
31
+ ```
32
+
33
+ ## Required Configuration
34
+
35
+ ### OpenAI API Key
36
+
37
+ The AI Imaging Agent requires an OpenAI API key for the vision-language model:
38
+
39
+ 1. Sign up for an account at [OpenAI](https://platform.openai.com/)
40
+ 2. Navigate to [API Keys](https://platform.openai.com/api-keys)
41
+ 3. Create a new API key
42
+ 4. Add it to your `.env` file:
43
+
44
+ ```dotenv
45
+ OPENAI_API_KEY=sk-your-actual-key-here
46
+ ```
47
+
48
+ ## Model Configuration
49
+
50
+ The agent model can be configured via `config.yaml`:
51
+
52
+ ```yaml
53
+ # AI Agent Model Configuration
54
+
55
+ # Default/fallback model (used for CLI and initial startup)
56
+ agent_model:
57
+ name: "gpt-4o-mini"
58
+ base_url: null # null for default OpenAI endpoint
59
+ api_key_env: "OPENAI_API_KEY"
60
+
61
+ # Available models for UI dropdown
62
+ available_models:
63
+ - display_name: "gpt-4o-mini"
64
+ name: "gpt-4o-mini"
65
+ base_url: null
66
+ provider: "OpenAI"
67
+ api_key_env: "OPENAI_API_KEY"
68
+
69
+ - display_name: "gpt-4o"
70
+ name: "gpt-4o"
71
+ base_url: null
72
+ provider: "OpenAI"
73
+ api_key_env: "OPENAI_API_KEY"
74
+
75
+ - display_name: "gpt-5.1"
76
+ name: "gpt-5.1"
77
+ base_url: null
78
+ provider: "OpenAI"
79
+ api_key_env: "OPENAI_API_KEY"
80
+ ```
81
+
82
+ ### Using Alternative Model Providers
83
+
84
+ You can configure custom OpenAI-compatible endpoints:
85
+
86
+ ```yaml
87
+ available_models:
88
+ - display_name: "EPFL Inference"
89
+ name: "gpt-4o-mini"
90
+ base_url: "https://inference.epfl.ch/v1"
91
+ provider: "EPFL"
92
+ api_key_env: "EPFL_API_KEY"
93
+ ```
94
+
95
+ Then add the corresponding API key to your `.env`:
96
+
97
+ ```dotenv
98
+ EPFL_API_KEY=your-epfl-key
99
+ ```
100
+
101
+ ## Optional Configuration
102
+
103
+ ### GitHub Token
104
+
105
+ For the repository info tool (optional):
106
+
107
+ ```dotenv
108
+ GITHUB_TOKEN=ghp_your_github_personal_access_token
109
+ ```
110
+
111
+ This enables the agent to fetch detailed information about GitHub repositories.
112
+
113
+ ### Pipeline Parameters
114
+
115
+ Adjust retrieval and recommendation settings directly in the app setting. You can change the `TOP_K` and `NUM_CHOICES` parameters.
116
+
117
+ ### Logging
118
+
119
+ Configure logging behavior:
120
+
121
+ ```dotenv
122
+ # Console log level (DEBUG, INFO, WARNING, ERROR)
123
+ LOGLEVEL_CONSOLE=WARNING
124
+
125
+ # File log level
126
+ LOGLEVEL_FILE=INFO
127
+
128
+ # Enable file logging (0 or 1)
129
+ FILE_LOG=1
130
+
131
+ # Log directory
132
+ LOG_DIR=logs
133
+
134
+ # Save VLM prompts and images for debugging (0 or 1)
135
+ LOG_PROMPTS=0
136
+ ```
137
+
138
+ !!! tip "Debug Mode"
139
+ Set `LOG_PROMPTS=1` to save VLM prompts and images to the `logs/` directory. This is useful for debugging but will increase disk usage.
140
+
141
+ ### Software Catalog
142
+
143
+ Specify the path to your software catalog:
144
+
145
+ ```dotenv
146
+ SOFTWARE_CATALOG=dataset/catalog.jsonl
147
+ ```
148
+
149
+ The catalog should be in JSONL format following the schema.org SoftwareSourceCode structure.
150
+
151
+ ## Verification
152
+
153
+ After configuring, verify your setup:
154
+
155
+ ```bash
156
+ # Check that environment variables are loaded
157
+ python -c "from dotenv import load_dotenv; import os; load_dotenv(); print('API Key:', 'SET' if os.getenv('OPENAI_API_KEY') else 'NOT SET')"
158
+ ```
159
+
160
+ ## Next Steps
161
+
162
+ With configuration complete, you're ready to:
163
+
164
+ - [Run the Quick Start](quickstart.md)
165
+ - Learn about [Using the Chat Interface](../user-guide/chat-interface.md)
docs/getting-started/installation.md CHANGED
@@ -1,143 +1,143 @@
1
- # Installation
2
-
3
- This guide will help you install and set up the AI Imaging Agent on your system.
4
-
5
- ## Prerequisites
6
-
7
- Before installing, ensure you have:
8
-
9
- - **Python 3.10–3.12** installed
10
- - **pip** (Python package manager)
11
- - **OpenAI API key** (or compatible API endpoint)
12
- - Internet connection for model calls
13
-
14
- ## Installation Steps
15
-
16
- ### 1. Clone the Repository
17
-
18
- ```bash
19
- git clone https://github.com/imaging-plaza/ai-agent.git
20
- cd ai-agent
21
- ```
22
-
23
- ### 2. Create Virtual Environment
24
-
25
- It's recommended to use a virtual environment to isolate dependencies:
26
-
27
- === "Linux/macOS"
28
-
29
- ```bash
30
- python -m venv .venv
31
- source .venv/bin/activate
32
- ```
33
-
34
- === "Windows"
35
-
36
- ```bash
37
- python -m venv .venv
38
- .venv\Scripts\activate
39
- ```
40
-
41
- ### 3. Install the Package
42
-
43
- For regular use:
44
-
45
- ```bash
46
- pip install --upgrade pip
47
- pip install -e .
48
- ```
49
-
50
- For development (includes test dependencies):
51
-
52
- ```bash
53
- pip install -e ".[dev]"
54
- ```
55
-
56
- ## Verify Installation
57
-
58
- Verify that the installation was successful:
59
-
60
- ```bash
61
- ai_agent --help
62
- ```
63
-
64
- You should see the available commands:
65
-
66
- ```
67
- usage: ai_agent [-h] {chat,sync}
68
-
69
- AI Agent CLI
70
-
71
- positional arguments:
72
- {chat,sync} 'chat' launches the chat UI; 'sync' runs one catalog refresh.
73
- ```
74
-
75
- ## Next Steps
76
-
77
- Now that you have installed the AI Imaging Agent, proceed to:
78
-
79
- - [Configuration](configuration.md) - Set up your environment and API keys
80
- - [Quick Start](quickstart.md) - Run your first query
81
-
82
- ## Troubleshooting
83
-
84
- ### Python Version Issues
85
-
86
- If you encounter issues with Python version compatibility:
87
-
88
- ```bash
89
- # Check your Python version
90
- python --version
91
-
92
- # Use a specific Python version
93
- python3.10 -m venv .venv
94
- ```
95
-
96
- ### Installation Errors
97
-
98
- If you encounter dependency conflicts:
99
-
100
- ```bash
101
- # Upgrade pip first
102
- pip install --upgrade pip setuptools wheel
103
-
104
- # Try installing again
105
- pip install -e .
106
- ```
107
-
108
- ### Missing System Dependencies
109
-
110
- Some packages may require system libraries:
111
-
112
- === "Ubuntu/Debian"
113
-
114
- ```bash
115
- sudo apt-get update
116
- sudo apt-get install python3-dev build-essential
117
- ```
118
-
119
- === "macOS"
120
-
121
- ```bash
122
- # Using Homebrew
123
- brew install python@3.10
124
- ```
125
-
126
- === "Windows"
127
-
128
- Ensure you have [Microsoft C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) installed.
129
-
130
- ## Docker Installation (Alternative)
131
-
132
- A Dockerfile is available for containerized deployment:
133
-
134
- ```bash
135
- # Build the Docker image
136
- docker build -t ai-agent -f Dockerfile .
137
-
138
- # Run the container
139
- docker run -p 7860:7860 --env-file .env ai-agent
140
- ```
141
-
142
- !!! note
143
- Make sure to create a `.env` file with your configuration before running the Docker container.
 
1
+ # Installation
2
+
3
+ This guide will help you install and set up the AI Imaging Agent on your system.
4
+
5
+ ## Prerequisites
6
+
7
+ Before installing, ensure you have:
8
+
9
+ - **Python 3.10–3.12** installed
10
+ - **pip** (Python package manager)
11
+ - **OpenAI API key** (or compatible API endpoint)
12
+ - Internet connection for model calls
13
+
14
+ ## Installation Steps
15
+
16
+ ### 1. Clone the Repository
17
+
18
+ ```bash
19
+ git clone https://github.com/imaging-plaza/ai-agent.git
20
+ cd ai-agent
21
+ ```
22
+
23
+ ### 2. Create Virtual Environment
24
+
25
+ It's recommended to use a virtual environment to isolate dependencies:
26
+
27
+ === "Linux/macOS"
28
+
29
+ ```bash
30
+ python -m venv .venv
31
+ source .venv/bin/activate
32
+ ```
33
+
34
+ === "Windows"
35
+
36
+ ```bash
37
+ python -m venv .venv
38
+ .venv\Scripts\activate
39
+ ```
40
+
41
+ ### 3. Install the Package
42
+
43
+ For regular use:
44
+
45
+ ```bash
46
+ pip install --upgrade pip
47
+ pip install -e .
48
+ ```
49
+
50
+ For development (includes test dependencies):
51
+
52
+ ```bash
53
+ pip install -e ".[dev]"
54
+ ```
55
+
56
+ ## Verify Installation
57
+
58
+ Verify that the installation was successful:
59
+
60
+ ```bash
61
+ ai_agent --help
62
+ ```
63
+
64
+ You should see the available commands:
65
+
66
+ ```
67
+ usage: ai_agent [-h] {chat,sync}
68
+
69
+ AI Agent CLI
70
+
71
+ positional arguments:
72
+ {chat,sync} 'chat' launches the chat UI; 'sync' runs one catalog refresh.
73
+ ```
74
+
75
+ ## Next Steps
76
+
77
+ Now that you have installed the AI Imaging Agent, proceed to:
78
+
79
+ - [Configuration](configuration.md) - Set up your environment and API keys
80
+ - [Quick Start](quickstart.md) - Run your first query
81
+
82
+ ## Troubleshooting
83
+
84
+ ### Python Version Issues
85
+
86
+ If you encounter issues with Python version compatibility:
87
+
88
+ ```bash
89
+ # Check your Python version
90
+ python --version
91
+
92
+ # Use a specific Python version
93
+ python3.10 -m venv .venv
94
+ ```
95
+
96
+ ### Installation Errors
97
+
98
+ If you encounter dependency conflicts:
99
+
100
+ ```bash
101
+ # Upgrade pip first
102
+ pip install --upgrade pip setuptools wheel
103
+
104
+ # Try installing again
105
+ pip install -e .
106
+ ```
107
+
108
+ ### Missing System Dependencies
109
+
110
+ Some packages may require system libraries:
111
+
112
+ === "Ubuntu/Debian"
113
+
114
+ ```bash
115
+ sudo apt-get update
116
+ sudo apt-get install python3-dev build-essential
117
+ ```
118
+
119
+ === "macOS"
120
+
121
+ ```bash
122
+ # Using Homebrew
123
+ brew install python@3.10
124
+ ```
125
+
126
+ === "Windows"
127
+
128
+ Ensure you have [Microsoft C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) installed.
129
+
130
+ ## Docker Installation (Alternative)
131
+
132
+ A Dockerfile is available for containerized deployment:
133
+
134
+ ```bash
135
+ # Build the Docker image
136
+ docker build -t ai-agent -f Dockerfile .
137
+
138
+ # Run the container
139
+ docker run -p 7860:7860 --env-file .env ai-agent
140
+ ```
141
+
142
+ !!! note
143
+ Make sure to create a `.env` file with your configuration before running the Docker container.
docs/getting-started/quickstart.md CHANGED
@@ -1,190 +1,190 @@
1
- # Quick Start
2
-
3
- This guide will walk you through your first interaction with the AI Imaging Agent.
4
-
5
- ## Starting the Application
6
-
7
- Once you've [installed](installation.md) and [configured](configuration.md) the agent, start the chat interface:
8
-
9
- ```bash
10
- ai_agent chat
11
- ```
12
-
13
- You should see output like:
14
-
15
- ```
16
- Running on local URL: http://127.0.0.1:7860
17
- ```
18
-
19
- Open your web browser and navigate to **http://127.0.0.1:7860**
20
-
21
- ## Your First Query
22
-
23
- ### Example 1: Object Segmentation
24
-
25
- Let's try a simple segmentation task:
26
-
27
- 1. **Upload an Image**: Click the upload area or drag and drop an image (e.g., a photo of a cat)
28
-
29
- 2. **Type Your Request**: In the chat input, type:
30
- ```
31
- I want to segment the cat from this image
32
- ```
33
-
34
- 3. **Review Recommendations**: The agent will return ranked tool recommendations with:
35
- - Tool names and descriptions
36
- - Accuracy scores
37
- - Explanations for why each tool matches your task
38
- - Links to runnable demos
39
-
40
- 4. **Run a Demo** (optional): Click the "Run Demo" button to see the result of the tool on your uploaded image
41
-
42
- ### Example 2: Medical Image Analysis
43
-
44
- For medical imaging tasks:
45
-
46
- 1. **Upload a Medical Image**: Upload a DICOM file, NIfTI volume, or medical image
47
-
48
- 2. **Describe Your Task**:
49
- ```
50
- Segment the lungs from this CT scan
51
- ```
52
-
53
- 3. **Get Format-Aware Results**: The agent considers:
54
- - Your image format (DICOM, NIfTI, etc.)
55
- - Image dimensions (2D, 3D, 4D)
56
- - Medical imaging modality (CT, MRI, etc.)
57
-
58
- ### Example 3: General Computer Vision
59
-
60
- For general tasks:
61
-
62
- ```
63
- Detect all objects in this image
64
- ```
65
-
66
- ```
67
- Extract text from this document image
68
- ```
69
-
70
- ```
71
- Classify what type of animal is in this picture
72
- ```
73
-
74
- ## Understanding the Interface
75
-
76
- ### Chat Panel
77
-
78
- - **Message History**: Scroll to see previous interactions
79
- - **Rich Media**: Images, files, and tool cards are rendered inline
80
- - **Code Blocks**: Formatted code and JSON responses
81
-
82
- ### Sidebar
83
-
84
- - **Uploaded Files**: View all files you've uploaded in the session
85
- - **Preview Images**: See converted image previews
86
- - **Debug Info**: View conversation state and excluded tools (if in debug mode)
87
-
88
- ### Tool Recommendation Cards
89
-
90
- Each recommended tool shows:
91
-
92
- - **Rank**: Priority order (1 = best match)
93
- - **Name**: Tool/software name
94
- - **Accuracy Score**: Confidence level (0-100%)
95
- - **Description**: What the tool does
96
- - **Explanation**: Why it matches your request
97
- - **Metadata**:
98
- - Supported modalities (CT, MRI, etc.)
99
- - Dimensions (2D, 3D, etc.)
100
- - File formats (DICOM, NIfTI, PNG, etc.)
101
- - License information
102
- - Tags and categories
103
- - **Demo Link**: Direct link to runnable example
104
-
105
- ## Advanced Usage
106
-
107
- ### Multi-Turn Conversations
108
-
109
- The agent maintains conversation context:
110
-
111
- ```
112
- You: I have a lung CT scan
113
- Agent: [Provides general information about lung CT analysis tools]
114
-
115
- You: I want to segment the airways
116
- Agent: [Provides specific airway segmentation tools]
117
-
118
- You: Show me alternatives
119
- Agent: [Provides different tool options]
120
- ```
121
-
122
- ### Excluding Tools
123
-
124
- Exclude specific tools from results:
125
-
126
- ```
127
- Find lung segmentation tools [EXCLUDE:totalsegmentator|medicalsam]
128
- ```
129
-
130
- ### Requesting Alternatives
131
-
132
- If initial results don't match your needs:
133
-
134
- ```
135
- Show me alternative tools
136
-
137
- Can you search for other options?
138
-
139
- What else is available?
140
- ```
141
-
142
- ## CLI Commands
143
-
144
- The agent provides two main commands:
145
-
146
- ### Launch Chat Interface
147
-
148
- ```bash
149
- ai_agent chat
150
- ```
151
-
152
- Starts the Gradio web interface with automatic catalog synchronization.
153
-
154
- ### Sync Catalog
155
-
156
- ```bash
157
- ai_agent sync
158
- ```
159
-
160
- Manually synchronize the software catalog without launching the UI.
161
-
162
- ## Tips for Best Results
163
-
164
- !!! tip "Be Specific"
165
- The more specific your request, the better the recommendations:
166
-
167
- - ❌ "Process this image"
168
- - ✅ "Segment the liver from this abdominal CT scan"
169
-
170
- !!! tip "Upload First"
171
- Upload your image before describing the task. The agent can see image content and metadata.
172
-
173
- !!! tip "Mention Formats"
174
- If you need specific format support, mention it:
175
-
176
- "I need a tool that works with DICOM files"
177
-
178
- !!! tip "Use Natural Language"
179
- No need to use technical jargon - conversational language works fine:
180
-
181
- "Help me find tumors in this MRI" works just as well as "Tumor detection in MRI volumes"
182
-
183
- ## Next Steps
184
-
185
- Now that you've run your first queries:
186
-
187
- - Learn more about [Using the Chat Interface](../user-guide/chat-interface.md)
188
- - Explore [Supported File Formats](../user-guide/file-formats.md)
189
- - Understand [How Recommendations Work](../user-guide/recommendations.md)
190
- - Dive into the [Architecture Overview](../architecture/overview.md)
 
1
+ # Quick Start
2
+
3
+ This guide will walk you through your first interaction with the AI Imaging Agent.
4
+
5
+ ## Starting the Application
6
+
7
+ Once you've [installed](installation.md) and [configured](configuration.md) the agent, start the chat interface:
8
+
9
+ ```bash
10
+ ai_agent chat
11
+ ```
12
+
13
+ You should see output like:
14
+
15
+ ```
16
+ Running on local URL: http://127.0.0.1:7860
17
+ ```
18
+
19
+ Open your web browser and navigate to **http://127.0.0.1:7860**
20
+
21
+ ## Your First Query
22
+
23
+ ### Example 1: Object Segmentation
24
+
25
+ Let's try a simple segmentation task:
26
+
27
+ 1. **Upload an Image**: Click the upload area or drag and drop an image (e.g., a photo of a cat)
28
+
29
+ 2. **Type Your Request**: In the chat input, type:
30
+ ```
31
+ I want to segment the cat from this image
32
+ ```
33
+
34
+ 3. **Review Recommendations**: The agent will return ranked tool recommendations with:
35
+ - Tool names and descriptions
36
+ - Accuracy scores
37
+ - Explanations for why each tool matches your task
38
+ - Links to runnable demos
39
+
40
+ 4. **Run a Demo** (optional): Click the "Run Demo" button to see the result of the tool on your uploaded image
41
+
42
+ ### Example 2: Medical Image Analysis
43
+
44
+ For medical imaging tasks:
45
+
46
+ 1. **Upload a Medical Image**: Upload a DICOM file, NIfTI volume, or medical image
47
+
48
+ 2. **Describe Your Task**:
49
+ ```
50
+ Segment the lungs from this CT scan
51
+ ```
52
+
53
+ 3. **Get Format-Aware Results**: The agent considers:
54
+ - Your image format (DICOM, NIfTI, etc.)
55
+ - Image dimensions (2D, 3D, 4D)
56
+ - Medical imaging modality (CT, MRI, etc.)
57
+
58
+ ### Example 3: General Computer Vision
59
+
60
+ For general tasks:
61
+
62
+ ```
63
+ Detect all objects in this image
64
+ ```
65
+
66
+ ```
67
+ Extract text from this document image
68
+ ```
69
+
70
+ ```
71
+ Classify what type of animal is in this picture
72
+ ```
73
+
74
+ ## Understanding the Interface
75
+
76
+ ### Chat Panel
77
+
78
+ - **Message History**: Scroll to see previous interactions
79
+ - **Rich Media**: Images, files, and tool cards are rendered inline
80
+ - **Code Blocks**: Formatted code and JSON responses
81
+
82
+ ### Sidebar
83
+
84
+ - **Uploaded Files**: View all files you've uploaded in the session
85
+ - **Preview Images**: See converted image previews
86
+ - **Debug Info**: View conversation state and excluded tools (if in debug mode)
87
+
88
+ ### Tool Recommendation Cards
89
+
90
+ Each recommended tool shows:
91
+
92
+ - **Rank**: Priority order (1 = best match)
93
+ - **Name**: Tool/software name
94
+ - **Accuracy Score**: Confidence level (0-100%)
95
+ - **Description**: What the tool does
96
+ - **Explanation**: Why it matches your request
97
+ - **Metadata**:
98
+ - Supported modalities (CT, MRI, etc.)
99
+ - Dimensions (2D, 3D, etc.)
100
+ - File formats (DICOM, NIfTI, PNG, etc.)
101
+ - License information
102
+ - Tags and categories
103
+ - **Demo Link**: Direct link to runnable example
104
+
105
+ ## Advanced Usage
106
+
107
+ ### Multi-Turn Conversations
108
+
109
+ The agent maintains conversation context:
110
+
111
+ ```
112
+ You: I have a lung CT scan
113
+ Agent: [Provides general information about lung CT analysis tools]
114
+
115
+ You: I want to segment the airways
116
+ Agent: [Provides specific airway segmentation tools]
117
+
118
+ You: Show me alternatives
119
+ Agent: [Provides different tool options]
120
+ ```
121
+
122
+ ### Excluding Tools
123
+
124
+ Exclude specific tools from results:
125
+
126
+ ```
127
+ Find lung segmentation tools [EXCLUDE:totalsegmentator|medicalsam]
128
+ ```
129
+
130
+ ### Requesting Alternatives
131
+
132
+ If initial results don't match your needs:
133
+
134
+ ```
135
+ Show me alternative tools
136
+
137
+ Can you search for other options?
138
+
139
+ What else is available?
140
+ ```
141
+
142
+ ## CLI Commands
143
+
144
+ The agent provides two main commands:
145
+
146
+ ### Launch Chat Interface
147
+
148
+ ```bash
149
+ ai_agent chat
150
+ ```
151
+
152
+ Starts the Gradio web interface with automatic catalog synchronization.
153
+
154
+ ### Sync Catalog
155
+
156
+ ```bash
157
+ ai_agent sync
158
+ ```
159
+
160
+ Manually synchronize the software catalog without launching the UI.
161
+
162
+ ## Tips for Best Results
163
+
164
+ !!! tip "Be Specific"
165
+ The more specific your request, the better the recommendations:
166
+
167
+ - ❌ "Process this image"
168
+ - ✅ "Segment the liver from this abdominal CT scan"
169
+
170
+ !!! tip "Upload First"
171
+ Upload your image before describing the task. The agent can see image content and metadata.
172
+
173
+ !!! tip "Mention Formats"
174
+ If you need specific format support, mention it:
175
+
176
+ "I need a tool that works with DICOM files"
177
+
178
+ !!! tip "Use Natural Language"
179
+ No need to use technical jargon - conversational language works fine:
180
+
181
+ "Help me find tumors in this MRI" works just as well as "Tumor detection in MRI volumes"
182
+
183
+ ## Next Steps
184
+
185
+ Now that you've run your first queries:
186
+
187
+ - Learn more about [Using the Chat Interface](../user-guide/chat-interface.md)
188
+ - Explore [Supported File Formats](../user-guide/file-formats.md)
189
+ - Understand [How Recommendations Work](../user-guide/recommendations.md)
190
+ - Dive into the [Architecture Overview](../architecture/overview.md)
docs/guide.md CHANGED
@@ -1,277 +1,277 @@
1
- # Project Guide
2
-
3
- This guide is a practical map of the entire repository for contributors and maintainers.
4
-
5
- It focuses on:
6
- - What each folder is responsible for
7
- - Which Python environment and package workflow are the defaults
8
- - Which commands are currently valid
9
- - What to improve next in architecture, testing, performance, and developer experience
10
-
11
- ## 1) System Summary
12
-
13
- AI Imaging Agent is a RAG plus VLM recommender for imaging software.
14
-
15
- High-level flow:
16
- 1. User uploads file(s) and enters a task.
17
- 2. Retrieval stage finds candidate tools (BGE-M3 + FAISS + reranker).
18
- 3. Agent/VLM stage ranks candidates with image-aware reasoning.
19
- 4. UI renders ranked recommendations and optional demo links.
20
-
21
- Primary orchestrator: [src/ai_agent/api/pipeline.py](src/ai_agent/api/pipeline.py)
22
-
23
- ## 2) Default Python Environment And Packages (Dev Container Canonical)
24
-
25
- Assume development is done inside the dev container.
26
-
27
- Source of truth:
28
- - Dev container: [.devcontainer/devcontainer.json](../.devcontainer/devcontainer.json)
29
- - Package metadata and pinned dependencies: [pyproject.toml](../pyproject.toml)
30
- - Secondary dependency list: [requirements.txt](../requirements.txt)
31
-
32
- Default environment:
33
- - OS: Debian Bookworm (dev container)
34
- - Python: 3.12
35
- - Environment manager: uv
36
- - Virtual environment path: .venv
37
-
38
- Recommended commands:
39
-
40
- ```bash
41
- uv venv
42
- uv pip install -e .
43
- uv pip install -e ".[dev]"
44
- ```
45
-
46
- Run and test:
47
-
48
- ```bash
49
- ai_agent chat
50
- ai_agent sync
51
- pytest tests/
52
- ```
53
-
54
- Important note on command drift:
55
- - CLI officially supports `chat` and `sync` in [src/ai_agent/cli.py](../src/ai_agent/cli.py).
56
- - [justfile](../justfile) currently references `ai_agent ui`, which does not match current CLI modes.
57
- - Documentation in this guide follows the actual CLI implementation.
58
-
59
- ## 3) Repository Top-Level Map
60
-
61
- - [.github/](../.github/): automation and agent instructions
62
- - [.devcontainer/](../.devcontainer/): dev container build and editor defaults
63
- - [docs/](.): MkDocs source pages
64
- - [src/](../src/): application source code
65
- - [tests/](../tests/): test suite
66
- - [data/](../data/): sample data assets
67
- - [tools/](../tools/): container/tooling helpers
68
- - [CHANGELOG.md](../CHANGELOG.md): release history
69
- - [config.yaml](../config.yaml): model/provider configuration
70
- - [mkdocs.yml](../mkdocs.yml): docs site navigation and theme
71
- - [pyproject.toml](../pyproject.toml): package metadata, dependencies, entrypoints
72
-
73
- ## 4) Detailed Source Folder Responsibilities
74
-
75
- Package root: [src/ai_agent/](../src/ai_agent)
76
-
77
- ### 4.1 [src/ai_agent/agent/](../src/ai_agent/agent)
78
-
79
- Purpose: conversational orchestration using PydanticAI.
80
-
81
- Key files:
82
- - [src/ai_agent/agent/agent.py](../src/ai_agent/agent/agent.py): agent setup, tool wiring, response flow
83
- - [src/ai_agent/agent/models.py](../src/ai_agent/agent/models.py): state/output models
84
- - [src/ai_agent/agent/utils.py](../src/ai_agent/agent/utils.py): helper utilities and guardrails
85
- - [src/ai_agent/agent/tools/](../src/ai_agent/agent/tools): concrete tool implementations
86
- - [src/ai_agent/agent/tools/mcp/](../src/ai_agent/agent/tools/mcp): MCP adapters
87
-
88
- Boundary:
89
- - Should orchestrate tools and policy, not own retrieval internals.
90
-
91
- ### 4.2 [src/ai_agent/api/](../src/ai_agent/api)
92
-
93
- Purpose: pipeline orchestration between inputs, retrieval, and selection.
94
-
95
- Key file:
96
- - [src/ai_agent/api/pipeline.py](../src/ai_agent/api/pipeline.py)
97
-
98
- Responsibilities:
99
- - validate files
100
- - extract metadata
101
- - build retrieval query
102
- - call retrieval and selection stages
103
- - manage index refresh/reload behavior
104
-
105
- Boundary:
106
- - Keep UI concerns out of this module.
107
-
108
- ### 4.3 [src/ai_agent/retriever/](../src/ai_agent/retriever)
109
-
110
- Purpose: deterministic retrieval stack (no LLM calls).
111
-
112
- Key files:
113
- - [src/ai_agent/retriever/text_embedder.py](../src/ai_agent/retriever/text_embedder.py)
114
- - [src/ai_agent/retriever/vector_index.py](../src/ai_agent/retriever/vector_index.py)
115
- - [src/ai_agent/retriever/reranker.py](../src/ai_agent/retriever/reranker.py)
116
- - [src/ai_agent/retriever/software_doc.py](../src/ai_agent/retriever/software_doc.py)
117
-
118
- Boundary:
119
- - Retrieval quality logic should stay here.
120
-
121
- ### 4.4 [src/ai_agent/generator/](../src/ai_agent/generator)
122
-
123
- Purpose: selection schema and prompting primitives.
124
-
125
- Key files:
126
- - [src/ai_agent/generator/prompts.py](../src/ai_agent/generator/prompts.py)
127
- - [src/ai_agent/generator/schema.py](../src/ai_agent/generator/schema.py)
128
-
129
- Boundary:
130
- - Keep this layer focused on schema and prompt contracts, not transport/UI concerns.
131
-
132
- ### 4.5 [src/ai_agent/ui/](../src/ai_agent/ui)
133
-
134
- Purpose: Gradio app and interaction handling.
135
-
136
- Key files:
137
- - [src/ai_agent/ui/app.py](../src/ai_agent/ui/app.py)
138
- - [src/ai_agent/ui/handlers.py](../src/ai_agent/ui/handlers.py)
139
- - [src/ai_agent/ui/components.py](../src/ai_agent/ui/components.py)
140
- - [src/ai_agent/ui/formatters.py](../src/ai_agent/ui/formatters.py)
141
- - [src/ai_agent/ui/state.py](../src/ai_agent/ui/state.py)
142
- - [src/ai_agent/ui/visualizations.py](../src/ai_agent/ui/visualizations.py)
143
-
144
- Boundary:
145
- - UI should call orchestrators, not reimplement retrieval/selection decisions.
146
-
147
- ### 4.6 [src/ai_agent/utils/](../src/ai_agent/utils)
148
-
149
- Purpose: cross-cutting utility functions.
150
-
151
- Key files:
152
- - [src/ai_agent/utils/config.py](../src/ai_agent/utils/config.py)
153
- - [src/ai_agent/utils/file_validator.py](../src/ai_agent/utils/file_validator.py)
154
- - [src/ai_agent/utils/image_meta.py](../src/ai_agent/utils/image_meta.py)
155
- - [src/ai_agent/utils/image_io.py](../src/ai_agent/utils/image_io.py)
156
- - [src/ai_agent/utils/previews.py](../src/ai_agent/utils/previews.py)
157
- - [src/ai_agent/utils/tags.py](../src/ai_agent/utils/tags.py)
158
- - [src/ai_agent/utils/temp_file_manager.py](../src/ai_agent/utils/temp_file_manager.py)
159
-
160
- Boundary:
161
- - Keep utilities reusable and independent from UI-specific logic.
162
-
163
- ### 4.7 [src/ai_agent/catalog/](../src/ai_agent/catalog)
164
-
165
- Purpose: catalog synchronization and refresh helpers.
166
-
167
- Key file:
168
- - [src/ai_agent/catalog/sync.py](../src/ai_agent/catalog/sync.py)
169
-
170
- Boundary:
171
- - Catalog IO and sync logic should stay isolated from ranking logic.
172
-
173
- ### 4.8 [src/ai_agent/core/](../src/ai_agent/core)
174
-
175
- Purpose: shared core coordination such as pipeline registry.
176
-
177
- Key file:
178
- - [src/ai_agent/core/pipeline_registry.py](../src/ai_agent/core/pipeline_registry.py)
179
-
180
- Boundary:
181
- - Keep core primitives minimal and dependency-light.
182
-
183
- ### 4.9 [src/ai_agent/queries/](../src/ai_agent/queries)
184
-
185
- Purpose: query assets used by catalog sync/retrieval support.
186
-
187
- Key file:
188
- - [src/ai_agent/queries/get_relevant_software.rq](../src/ai_agent/queries/get_relevant_software.rq)
189
-
190
- Boundary:
191
- - Keep query definitions versioned and testable.
192
-
193
- ### 4.10 [src/ai_agent/cli.py](../src/ai_agent/cli.py)
194
-
195
- Purpose: command entry point and mode dispatch.
196
-
197
- Current modes:
198
- - `chat`
199
- - `sync`
200
-
201
- This is the command contract docs should follow.
202
-
203
- ## 5) Supporting Folders
204
-
205
- ### 5.1 [tests/](../tests)
206
-
207
- Contains unit/integration tests and test fixtures under [tests/data/](../tests/data).
208
-
209
- Improvement target:
210
- - add more focused tests for UI handler edge cases and tool failure handling.
211
-
212
- ### 5.2 [tools/](../tools)
213
-
214
- Container and deployment support assets.
215
-
216
- Notable file:
217
- - [tools/image/Dockerfile](../tools/image/Dockerfile) (uv + Python 3.12 baseline)
218
-
219
- ### 5.3 [docs/](.)
220
-
221
- Documentation source for MkDocs.
222
-
223
- Add new pages to [mkdocs.yml](../mkdocs.yml) nav to keep docs discoverable.
224
-
225
- ## 6) Known Inconsistencies To Track
226
-
227
- 1. [justfile](../justfile) uses `ai_agent ui`, while [src/ai_agent/cli.py](../src/ai_agent/cli.py) defines `chat` and `sync`.
228
- 2. Installation docs often show pip-first flow, while dev container bootstrap is uv-first.
229
- 3. [requirements.txt](../requirements.txt) is looser than [pyproject.toml](../pyproject.toml), which contains current pinned/runtime dependencies.
230
-
231
- ## 7) Codebase Improvement Guidelines
232
-
233
- ### 7.1 Architecture And Modularity
234
-
235
- 1. Keep strict stage boundaries: retrieval logic in `retriever`, selection contracts in `generator`, orchestration in `api`.
236
- 2. Minimize cross-layer imports from `ui` to low-level modules.
237
- 3. Introduce lightweight interface contracts for tool adapters to reduce coupling in `agent/tools`.
238
- 4. Centralize shared constants/env defaults to reduce duplicated configuration behavior.
239
-
240
- ### 7.2 Testing And Quality Gates
241
-
242
- 1. Add regression tests for format-token query construction and retry broadening behavior.
243
- 2. Add failure-path tests for image preview generation and graceful degradation.
244
- 3. Add contract tests for agent tool outputs (search, alternative search, repo info).
245
- 4. Enforce formatting/lint/type checks in CI (`ruff`, `black --check`, `mypy`, `pytest`).
246
-
247
- ### 7.3 Performance And Retrieval Quality
248
-
249
- 1. Add benchmark fixtures for retrieval latency and reranker throughput.
250
- 2. Track retrieval quality with a small fixed evaluation set (top-k recall, MRR).
251
- 3. Cache expensive metadata extraction where safe for repeated files in a session.
252
- 4. Make index reload behavior observable with structured counters in logs.
253
-
254
- ### 7.4 Developer Experience And CI
255
-
256
- 1. Align `just` tasks with real CLI contract (`chat`/`sync`).
257
- 2. Add a docs link checker in CI to prevent markdown drift.
258
- 3. Document one canonical local workflow (dev container first, optional local pip fallback).
259
- 4. Add a short maintainer checklist for release prep and changelog updates.
260
-
261
- ## 8) Practical Contributor Checklist
262
-
263
- Before opening a PR:
264
- 1. Install/update in editable mode in the active environment.
265
- 2. Run tests relevant to changed modules.
266
- 3. Validate docs links if docs were touched.
267
- 4. Update [CHANGELOG.md](../CHANGELOG.md) for user-visible changes.
268
- 5. Confirm command and environment docs still match real behavior.
269
-
270
- ## 9) Related References
271
-
272
- - [README.md](../README.md)
273
- - [docs/index.md](index.md)
274
- - [docs/architecture/overview.md](architecture/overview.md)
275
- - [docs/development/structure.md](development/structure.md)
276
- - [AGENTS.md](../AGENTS.md)
277
- - [.github/copilot-instructions.md](../.github/copilot-instructions.md)
 
1
+ # Project Guide
2
+
3
+ This guide is a practical map of the entire repository for contributors and maintainers.
4
+
5
+ It focuses on:
6
+ - What each folder is responsible for
7
+ - Which Python environment and package workflow are the defaults
8
+ - Which commands are currently valid
9
+ - What to improve next in architecture, testing, performance, and developer experience
10
+
11
+ ## 1) System Summary
12
+
13
+ AI Imaging Agent is a RAG plus VLM recommender for imaging software.
14
+
15
+ High-level flow:
16
+ 1. User uploads file(s) and enters a task.
17
+ 2. Retrieval stage finds candidate tools (BGE-M3 + FAISS + reranker).
18
+ 3. Agent/VLM stage ranks candidates with image-aware reasoning.
19
+ 4. UI renders ranked recommendations and optional demo links.
20
+
21
+ Primary orchestrator: [src/ai_agent/api/pipeline.py](src/ai_agent/api/pipeline.py)
22
+
23
+ ## 2) Default Python Environment And Packages (Dev Container Canonical)
24
+
25
+ Assume development is done inside the dev container.
26
+
27
+ Source of truth:
28
+ - Dev container: [.devcontainer/devcontainer.json](../.devcontainer/devcontainer.json)
29
+ - Package metadata and pinned dependencies: [pyproject.toml](../pyproject.toml)
30
+ - Secondary dependency list: [requirements.txt](../requirements.txt)
31
+
32
+ Default environment:
33
+ - OS: Debian Bookworm (dev container)
34
+ - Python: 3.12
35
+ - Environment manager: uv
36
+ - Virtual environment path: .venv
37
+
38
+ Recommended commands:
39
+
40
+ ```bash
41
+ uv venv
42
+ uv pip install -e .
43
+ uv pip install -e ".[dev]"
44
+ ```
45
+
46
+ Run and test:
47
+
48
+ ```bash
49
+ ai_agent chat
50
+ ai_agent sync
51
+ pytest tests/
52
+ ```
53
+
54
+ Important note on command drift:
55
+ - CLI officially supports `chat` and `sync` in [src/ai_agent/cli.py](../src/ai_agent/cli.py).
56
+ - [justfile](../justfile) currently references `ai_agent ui`, which does not match current CLI modes.
57
+ - Documentation in this guide follows the actual CLI implementation.
58
+
59
+ ## 3) Repository Top-Level Map
60
+
61
+ - [.github/](../.github/): automation and agent instructions
62
+ - [.devcontainer/](../.devcontainer/): dev container build and editor defaults
63
+ - [docs/](.): MkDocs source pages
64
+ - [src/](../src/): application source code
65
+ - [tests/](../tests/): test suite
66
+ - [data/](../data/): sample data assets
67
+ - [tools/](../tools/): container/tooling helpers
68
+ - [CHANGELOG.md](../CHANGELOG.md): release history
69
+ - [config.yaml](../config.yaml): model/provider configuration
70
+ - [mkdocs.yml](../mkdocs.yml): docs site navigation and theme
71
+ - [pyproject.toml](../pyproject.toml): package metadata, dependencies, entrypoints
72
+
73
+ ## 4) Detailed Source Folder Responsibilities
74
+
75
+ Package root: [src/ai_agent/](../src/ai_agent)
76
+
77
+ ### 4.1 [src/ai_agent/agent/](../src/ai_agent/agent)
78
+
79
+ Purpose: conversational orchestration using PydanticAI.
80
+
81
+ Key files:
82
+ - [src/ai_agent/agent/agent.py](../src/ai_agent/agent/agent.py): agent setup, tool wiring, response flow
83
+ - [src/ai_agent/agent/models.py](../src/ai_agent/agent/models.py): state/output models
84
+ - [src/ai_agent/agent/utils.py](../src/ai_agent/agent/utils.py): helper utilities and guardrails
85
+ - [src/ai_agent/agent/tools/](../src/ai_agent/agent/tools): concrete tool implementations
86
+ - [src/ai_agent/agent/tools/mcp/](../src/ai_agent/agent/tools/mcp): MCP adapters
87
+
88
+ Boundary:
89
+ - Should orchestrate tools and policy, not own retrieval internals.
90
+
91
+ ### 4.2 [src/ai_agent/api/](../src/ai_agent/api)
92
+
93
+ Purpose: pipeline orchestration between inputs, retrieval, and selection.
94
+
95
+ Key file:
96
+ - [src/ai_agent/api/pipeline.py](../src/ai_agent/api/pipeline.py)
97
+
98
+ Responsibilities:
99
+ - validate files
100
+ - extract metadata
101
+ - build retrieval query
102
+ - call retrieval and selection stages
103
+ - manage index refresh/reload behavior
104
+
105
+ Boundary:
106
+ - Keep UI concerns out of this module.
107
+
108
+ ### 4.3 [src/ai_agent/retriever/](../src/ai_agent/retriever)
109
+
110
+ Purpose: deterministic retrieval stack (no LLM calls).
111
+
112
+ Key files:
113
+ - [src/ai_agent/retriever/text_embedder.py](../src/ai_agent/retriever/text_embedder.py)
114
+ - [src/ai_agent/retriever/vector_index.py](../src/ai_agent/retriever/vector_index.py)
115
+ - [src/ai_agent/retriever/reranker.py](../src/ai_agent/retriever/reranker.py)
116
+ - [src/ai_agent/retriever/software_doc.py](../src/ai_agent/retriever/software_doc.py)
117
+
118
+ Boundary:
119
+ - Retrieval quality logic should stay here.
120
+
121
+ ### 4.4 [src/ai_agent/generator/](../src/ai_agent/generator)
122
+
123
+ Purpose: selection schema and prompting primitives.
124
+
125
+ Key files:
126
+ - [src/ai_agent/generator/prompts.py](../src/ai_agent/generator/prompts.py)
127
+ - [src/ai_agent/generator/schema.py](../src/ai_agent/generator/schema.py)
128
+
129
+ Boundary:
130
+ - Keep this layer focused on schema and prompt contracts, not transport/UI concerns.
131
+
132
+ ### 4.5 [src/ai_agent/ui/](../src/ai_agent/ui)
133
+
134
+ Purpose: Gradio app and interaction handling.
135
+
136
+ Key files:
137
+ - [src/ai_agent/ui/app.py](../src/ai_agent/ui/app.py)
138
+ - [src/ai_agent/ui/handlers.py](../src/ai_agent/ui/handlers.py)
139
+ - [src/ai_agent/ui/components.py](../src/ai_agent/ui/components.py)
140
+ - [src/ai_agent/ui/formatters.py](../src/ai_agent/ui/formatters.py)
141
+ - [src/ai_agent/ui/state.py](../src/ai_agent/ui/state.py)
142
+ - [src/ai_agent/ui/visualizations.py](../src/ai_agent/ui/visualizations.py)
143
+
144
+ Boundary:
145
+ - UI should call orchestrators, not reimplement retrieval/selection decisions.
146
+
147
+ ### 4.6 [src/ai_agent/utils/](../src/ai_agent/utils)
148
+
149
+ Purpose: cross-cutting utility functions.
150
+
151
+ Key files:
152
+ - [src/ai_agent/utils/config.py](../src/ai_agent/utils/config.py)
153
+ - [src/ai_agent/utils/file_validator.py](../src/ai_agent/utils/file_validator.py)
154
+ - [src/ai_agent/utils/image_meta.py](../src/ai_agent/utils/image_meta.py)
155
+ - [src/ai_agent/utils/image_io.py](../src/ai_agent/utils/image_io.py)
156
+ - [src/ai_agent/utils/previews.py](../src/ai_agent/utils/previews.py)
157
+ - [src/ai_agent/utils/tags.py](../src/ai_agent/utils/tags.py)
158
+ - [src/ai_agent/utils/temp_file_manager.py](../src/ai_agent/utils/temp_file_manager.py)
159
+
160
+ Boundary:
161
+ - Keep utilities reusable and independent from UI-specific logic.
162
+
163
+ ### 4.7 [src/ai_agent/catalog/](../src/ai_agent/catalog)
164
+
165
+ Purpose: catalog synchronization and refresh helpers.
166
+
167
+ Key file:
168
+ - [src/ai_agent/catalog/sync.py](../src/ai_agent/catalog/sync.py)
169
+
170
+ Boundary:
171
+ - Catalog IO and sync logic should stay isolated from ranking logic.
172
+
173
+ ### 4.8 [src/ai_agent/core/](../src/ai_agent/core)
174
+
175
+ Purpose: shared core coordination such as pipeline registry.
176
+
177
+ Key file:
178
+ - [src/ai_agent/core/pipeline_registry.py](../src/ai_agent/core/pipeline_registry.py)
179
+
180
+ Boundary:
181
+ - Keep core primitives minimal and dependency-light.
182
+
183
+ ### 4.9 [src/ai_agent/queries/](../src/ai_agent/queries)
184
+
185
+ Purpose: query assets used by catalog sync/retrieval support.
186
+
187
+ Key file:
188
+ - [src/ai_agent/queries/get_relevant_software.rq](../src/ai_agent/queries/get_relevant_software.rq)
189
+
190
+ Boundary:
191
+ - Keep query definitions versioned and testable.
192
+
193
+ ### 4.10 [src/ai_agent/cli.py](../src/ai_agent/cli.py)
194
+
195
+ Purpose: command entry point and mode dispatch.
196
+
197
+ Current modes:
198
+ - `chat`
199
+ - `sync`
200
+
201
+ This is the command contract docs should follow.
202
+
203
+ ## 5) Supporting Folders
204
+
205
+ ### 5.1 [tests/](../tests)
206
+
207
+ Contains unit/integration tests and test fixtures under [tests/data/](../tests/data).
208
+
209
+ Improvement target:
210
+ - add more focused tests for UI handler edge cases and tool failure handling.
211
+
212
+ ### 5.2 [tools/](../tools)
213
+
214
+ Container and deployment support assets.
215
+
216
+ Notable file:
217
+ - [tools/image/Dockerfile](../tools/image/Dockerfile) (uv + Python 3.12 baseline)
218
+
219
+ ### 5.3 [docs/](.)
220
+
221
+ Documentation source for MkDocs.
222
+
223
+ Add new pages to [mkdocs.yml](../mkdocs.yml) nav to keep docs discoverable.
224
+
225
+ ## 6) Known Inconsistencies To Track
226
+
227
+ 1. [justfile](../justfile) uses `ai_agent ui`, while [src/ai_agent/cli.py](../src/ai_agent/cli.py) defines `chat` and `sync`.
228
+ 2. Installation docs often show pip-first flow, while dev container bootstrap is uv-first.
229
+ 3. [requirements.txt](../requirements.txt) is looser than [pyproject.toml](../pyproject.toml), which contains current pinned/runtime dependencies.
230
+
231
+ ## 7) Codebase Improvement Guidelines
232
+
233
+ ### 7.1 Architecture And Modularity
234
+
235
+ 1. Keep strict stage boundaries: retrieval logic in `retriever`, selection contracts in `generator`, orchestration in `api`.
236
+ 2. Minimize cross-layer imports from `ui` to low-level modules.
237
+ 3. Introduce lightweight interface contracts for tool adapters to reduce coupling in `agent/tools`.
238
+ 4. Centralize shared constants/env defaults to reduce duplicated configuration behavior.
239
+
240
+ ### 7.2 Testing And Quality Gates
241
+
242
+ 1. Add regression tests for format-token query construction and retry broadening behavior.
243
+ 2. Add failure-path tests for image preview generation and graceful degradation.
244
+ 3. Add contract tests for agent tool outputs (search, alternative search, repo info).
245
+ 4. Enforce formatting/lint/type checks in CI (`ruff`, `black --check`, `mypy`, `pytest`).
246
+
247
+ ### 7.3 Performance And Retrieval Quality
248
+
249
+ 1. Add benchmark fixtures for retrieval latency and reranker throughput.
250
+ 2. Track retrieval quality with a small fixed evaluation set (top-k recall, MRR).
251
+ 3. Cache expensive metadata extraction where safe for repeated files in a session.
252
+ 4. Make index reload behavior observable with structured counters in logs.
253
+
254
+ ### 7.4 Developer Experience And CI
255
+
256
+ 1. Align `just` tasks with real CLI contract (`chat`/`sync`).
257
+ 2. Add a docs link checker in CI to prevent markdown drift.
258
+ 3. Document one canonical local workflow (dev container first, optional local pip fallback).
259
+ 4. Add a short maintainer checklist for release prep and changelog updates.
260
+
261
+ ## 8) Practical Contributor Checklist
262
+
263
+ Before opening a PR:
264
+ 1. Install/update in editable mode in the active environment.
265
+ 2. Run tests relevant to changed modules.
266
+ 3. Validate docs links if docs were touched.
267
+ 4. Update [CHANGELOG.md](../CHANGELOG.md) for user-visible changes.
268
+ 5. Confirm command and environment docs still match real behavior.
269
+
270
+ ## 9) Related References
271
+
272
+ - [README.md](../README.md)
273
+ - [docs/index.md](index.md)
274
+ - [docs/architecture/overview.md](architecture/overview.md)
275
+ - [docs/development/structure.md](development/structure.md)
276
+ - [AGENTS.md](../AGENTS.md)
277
+ - [.github/copilot-instructions.md](../.github/copilot-instructions.md)
docs/index.md CHANGED
@@ -1,81 +1,81 @@
1
- # AI Imaging Agent
2
-
3
- **An intelligent RAG + AI agent system that helps users discover the right imaging software for their images and tasks.**
4
-
5
- [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
6
- [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/imaging-plaza/ai-agent/blob/main/LICENSE)
7
-
8
- ---
9
-
10
- ## What is AI Imaging Agent?
11
-
12
- AI Imaging Agent (also known as **Imaging Plaza**) is a conversational AI assistant that helps researchers and practitioners find the right imaging analysis tools for their specific needs. Simply upload an image, describe what you want to do, and get ranked software recommendations with links to runnable demos.
13
-
14
- ## ✨ Key Features
15
-
16
- - **🤖 Conversational AI Agent**: Natural language interaction with multi-turn context
17
- - **🔍 Smart Retrieval**: BGE-M3 embeddings + FAISS + CrossEncoder reranking
18
- - **👁️ Vision-Aware Selection**: VLM-based tool selection considering both image content and metadata
19
- - **🏥 Medical Imaging Focus**: Specialized support for CT, MRI, DICOM, NIfTI, and other medical formats
20
- - **🎯 Format-Aware Matching**: IO compatibility scoring based on file formats and dimensions
21
- - **🚀 Demo Integration**: Direct execution of Gradio Space demos on your images
22
- - **📊 Rich UI**: Chat interface with image previews, file management, and execution traces
23
-
24
- ## Quick Example
25
-
26
- ```bash
27
- # Install and run
28
- pip install -e .
29
- ai_agent chat
30
- ```
31
-
32
- Then in the web interface:
33
-
34
- 1. Upload an image (e.g., a CT scan, or a PNG)
35
- 2. Type your request (e.g., _"I want to segment the lungs from this image"_ or _"I want to deblur this image"_)
36
- 3. Get ranked tool recommendations with accuracy scores
37
- 4. Click "Run demo" to execute tools directly
38
-
39
- ## Use Cases
40
-
41
- ### Medical Imaging
42
- - Segment organs from CT/MRI scans
43
- - Register brain images
44
- - Detect tumors and anomalies
45
- - Analyze DICOM files
46
-
47
- ### Scientific Imaging
48
- - Process microscopy images
49
- - Analyze multidimensional TIFF stacks
50
- - Extract features from scientific images
51
-
52
- ### General Computer Vision
53
- - Object detection and segmentation
54
- - Image classification
55
- - OCR and text extraction
56
- - Image enhancement
57
-
58
- ## How It Works
59
-
60
- The system uses a **two-stage pipeline**:
61
-
62
- 1. **Retrieval Stage**: Fast text search using BGE-M3 embeddings and FAISS to find candidate tools from a curated catalog
63
- 2. **Agent Selection**: Vision-language model (GPT-4o) analyzes your image and task to rank the best tools with explanations
64
-
65
- Learn more in the [Architecture Overview](architecture/overview.md).
66
-
67
- ## Getting Started
68
-
69
- Ready to try it out? Head over to the [Installation Guide](getting-started/installation.md) to get started!
70
-
71
- ## Project Guide
72
-
73
- For maintainers and contributors, see the [Project Guide](guide.md) for a detailed repository map, dev-container environment defaults, and practical codebase improvement guidelines.
74
-
75
- ## Project Status
76
-
77
- This project is actively developed and maintained by the Imaging Plaza team. Check the [Changelog](reference/changelog.md) for recent updates.
78
-
79
- ## License
80
-
81
- This project is licensed under the Apache 2.0 License - see the [LICENSE](https://github.com/imaging-plaza/ai-agent/blob/main/LICENSE) file for details.
 
1
+ # AI Imaging Agent
2
+
3
+ **An intelligent RAG + AI agent system that helps users discover the right imaging software for their images and tasks.**
4
+
5
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
6
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/imaging-plaza/ai-agent/blob/main/LICENSE)
7
+
8
+ ---
9
+
10
+ ## What is AI Imaging Agent?
11
+
12
+ AI Imaging Agent (also known as **Imaging Plaza**) is a conversational AI assistant that helps researchers and practitioners find the right imaging analysis tools for their specific needs. Simply upload an image, describe what you want to do, and get ranked software recommendations with links to runnable demos.
13
+
14
+ ## ✨ Key Features
15
+
16
+ - **🤖 Conversational AI Agent**: Natural language interaction with multi-turn context
17
+ - **🔍 Smart Retrieval**: BGE-M3 embeddings + FAISS + CrossEncoder reranking
18
+ - **👁️ Vision-Aware Selection**: VLM-based tool selection considering both image content and metadata
19
+ - **🏥 Medical Imaging Focus**: Specialized support for CT, MRI, DICOM, NIfTI, and other medical formats
20
+ - **🎯 Format-Aware Matching**: IO compatibility scoring based on file formats and dimensions
21
+ - **🚀 Demo Integration**: Direct execution of Gradio Space demos on your images
22
+ - **📊 Rich UI**: Chat interface with image previews, file management, and execution traces
23
+
24
+ ## Quick Example
25
+
26
+ ```bash
27
+ # Install and run
28
+ pip install -e .
29
+ ai_agent chat
30
+ ```
31
+
32
+ Then in the web interface:
33
+
34
+ 1. Upload an image (e.g., a CT scan, or a PNG)
35
+ 2. Type your request (e.g., _"I want to segment the lungs from this image"_ or _"I want to deblur this image"_)
36
+ 3. Get ranked tool recommendations with accuracy scores
37
+ 4. Click "Run demo" to execute tools directly
38
+
39
+ ## Use Cases
40
+
41
+ ### Medical Imaging
42
+ - Segment organs from CT/MRI scans
43
+ - Register brain images
44
+ - Detect tumors and anomalies
45
+ - Analyze DICOM files
46
+
47
+ ### Scientific Imaging
48
+ - Process microscopy images
49
+ - Analyze multidimensional TIFF stacks
50
+ - Extract features from scientific images
51
+
52
+ ### General Computer Vision
53
+ - Object detection and segmentation
54
+ - Image classification
55
+ - OCR and text extraction
56
+ - Image enhancement
57
+
58
+ ## How It Works
59
+
60
+ The system uses a **two-stage pipeline**:
61
+
62
+ 1. **Retrieval Stage**: Fast text search using BGE-M3 embeddings and FAISS to find candidate tools from a curated catalog
63
+ 2. **Agent Selection**: Vision-language model (GPT-4o) analyzes your image and task to rank the best tools with explanations
64
+
65
+ Learn more in the [Architecture Overview](architecture/overview.md).
66
+
67
+ ## Getting Started
68
+
69
+ Ready to try it out? Head over to the [Installation Guide](getting-started/installation.md) to get started!
70
+
71
+ ## Project Guide
72
+
73
+ For maintainers and contributors, see the [Project Guide](guide.md) for a detailed repository map, dev-container environment defaults, and practical codebase improvement guidelines.
74
+
75
+ ## Project Status
76
+
77
+ This project is actively developed and maintained by the Imaging Plaza team. Check the [Changelog](reference/changelog.md) for recent updates.
78
+
79
+ ## License
80
+
81
+ This project is licensed under the Apache 2.0 License - see the [LICENSE](https://github.com/imaging-plaza/ai-agent/blob/main/LICENSE) file for details.
docs/reference/changelog.md CHANGED
@@ -1,79 +1,79 @@
1
- # Changelog
2
-
3
- All notable changes to the AI Imaging Agent are documented here.
4
-
5
- For the complete, detailed changelog, see [CHANGELOG.md](https://github.com/imaging-plaza/ai-agent/blob/main/CHANGELOG.md) in the repository.
6
-
7
- ## Recent Releases
8
-
9
- ### [1.0.0]
10
-
11
- #### 🚀 Added
12
-
13
- - **Chat-based interface** (`ai_agent chat`) with conversational AI assistant and tool integration
14
- - **Imaging Plaza UI**: Custom branding, theme, and improved layout
15
- - **Iterative retrieval with automatic retry** for low-result queries
16
- - **Alternative search tool** for agent-driven query refinement
17
- - **YAML configuration** (`config.yaml`) for flexible model and backend setup
18
- - **DeepWiki MCP integration** for fast GitHub repository documentation access
19
-
20
- ---
21
-
22
- #### 🔄 Changed
23
-
24
- - CLI updated: `ai_agent chat` replaces deprecated `ai_agent ui`
25
- - Retrieval pipeline enhanced with smarter expansion and retry logic
26
- - UI state management simplified
27
- - Agent-based architecture fully replaces legacy pipelines
28
-
29
- ---
30
-
31
- #### 🧹 Removed
32
-
33
- - `VLMToolSelector` (replaced by agent-based tool selection)
34
- - Legacy refine intent detection system
35
- - Deprecated UI command (`ai_agent ui`)
36
- - Outdated tests and unused code paths
37
-
38
- ### [0.1.3] - 2025-10-22
39
-
40
- #### Added
41
- - Gradio Space runner tool
42
- - Repository info tool
43
-
44
- #### Fixed
45
- - Gradio UI context binding
46
- - Chatbot message format migration
47
- - Cache cleaning
48
- - PNG preview handling
49
-
50
- ### [0.1.2] - 2025-10-07
51
-
52
- #### Added
53
- - Pydantic AI pipeline with tools
54
- - Better runnable example handling
55
-
56
- ### [0.1.1] - 2025-10-02
57
-
58
- #### Added
59
- - Experimental Pydantic AI agent skeleton
60
- - Multimodal agent pathway
61
-
62
- ### [0.1.0] - 2025-09-30
63
-
64
- #### Added
65
- - Initial chat functionality
66
-
67
- ## Versioning
68
-
69
- This project follows [Semantic Versioning](https://semver.org/):
70
-
71
- - **MAJOR**: Incompatible API changes
72
- - **MINOR**: New functionality (backwards-compatible)
73
- - **PATCH**: Bug fixes (backwards-compatible)
74
-
75
- ## Contributing
76
-
77
- All notable changes should be documented in [CHANGELOG.md](https://github.com/imaging-plaza/ai-agent/blob/main/CHANGELOG.md) following the [Keep a Changelog](https://keepachangelog.com/) format.
78
-
79
- See [Contributing Guide](../development/contributing.md) for details.
 
1
+ # Changelog
2
+
3
+ All notable changes to the AI Imaging Agent are documented here.
4
+
5
+ For the complete, detailed changelog, see [CHANGELOG.md](https://github.com/imaging-plaza/ai-agent/blob/main/CHANGELOG.md) in the repository.
6
+
7
+ ## Recent Releases
8
+
9
+ ### [1.0.0]
10
+
11
+ #### 🚀 Added
12
+
13
+ - **Chat-based interface** (`ai_agent chat`) with conversational AI assistant and tool integration
14
+ - **Imaging Plaza UI**: Custom branding, theme, and improved layout
15
+ - **Iterative retrieval with automatic retry** for low-result queries
16
+ - **Alternative search tool** for agent-driven query refinement
17
+ - **YAML configuration** (`config.yaml`) for flexible model and backend setup
18
+ - **DeepWiki MCP integration** for fast GitHub repository documentation access
19
+
20
+ ---
21
+
22
+ #### 🔄 Changed
23
+
24
+ - CLI updated: `ai_agent chat` replaces deprecated `ai_agent ui`
25
+ - Retrieval pipeline enhanced with smarter expansion and retry logic
26
+ - UI state management simplified
27
+ - Agent-based architecture fully replaces legacy pipelines
28
+
29
+ ---
30
+
31
+ #### 🧹 Removed
32
+
33
+ - `VLMToolSelector` (replaced by agent-based tool selection)
34
+ - Legacy refine intent detection system
35
+ - Deprecated UI command (`ai_agent ui`)
36
+ - Outdated tests and unused code paths
37
+
38
+ ### [0.1.3] - 2025-10-22
39
+
40
+ #### Added
41
+ - Gradio Space runner tool
42
+ - Repository info tool
43
+
44
+ #### Fixed
45
+ - Gradio UI context binding
46
+ - Chatbot message format migration
47
+ - Cache cleaning
48
+ - PNG preview handling
49
+
50
+ ### [0.1.2] - 2025-10-07
51
+
52
+ #### Added
53
+ - Pydantic AI pipeline with tools
54
+ - Better runnable example handling
55
+
56
+ ### [0.1.1] - 2025-10-02
57
+
58
+ #### Added
59
+ - Experimental Pydantic AI agent skeleton
60
+ - Multimodal agent pathway
61
+
62
+ ### [0.1.0] - 2025-09-30
63
+
64
+ #### Added
65
+ - Initial chat functionality
66
+
67
+ ## Versioning
68
+
69
+ This project follows [Semantic Versioning](https://semver.org/):
70
+
71
+ - **MAJOR**: Incompatible API changes
72
+ - **MINOR**: New functionality (backwards-compatible)
73
+ - **PATCH**: Bug fixes (backwards-compatible)
74
+
75
+ ## Contributing
76
+
77
+ All notable changes should be documented in [CHANGELOG.md](https://github.com/imaging-plaza/ai-agent/blob/main/CHANGELOG.md) following the [Keep a Changelog](https://keepachangelog.com/) format.
78
+
79
+ See [Contributing Guide](../development/contributing.md) for details.
docs/reference/cli.md CHANGED
@@ -1,197 +1,197 @@
1
- # CLI Commands
2
-
3
- The AI Imaging Agent provides a command-line interface for starting the application and managing the software catalog.
4
-
5
- ## Available Commands
6
-
7
- ### ai_agent chat
8
-
9
- Launch the chat-based user interface.
10
-
11
- ```bash
12
- ai_agent chat
13
- ```
14
-
15
- **What it does**:
16
-
17
- 1. Performs startup catalog synchronization
18
- 2. Loads the FAISS index
19
- 3. Initializes the retrieval and agent pipelines
20
- 4. Launches the Gradio web interface on `http://127.0.0.1:7860`
21
- 5. Starts background catalog refresh (if configured)
22
-
23
- **Options**: None (all configuration via `.env` and `config.yaml`)
24
-
25
- **Example**:
26
-
27
- ```bash
28
- $ ai_agent chat
29
- [startup-sync] 150 → dataset/catalog.jsonl
30
- [startup-refresh] catalog unchanged; keeping existing FAISS index
31
- Running on local URL: http://127.0.0.1:7860
32
-
33
- To create a public link, set `share=True` in `launch()`.
34
- ```
35
-
36
- **Background Refresh**:
37
-
38
- If `SYNC_EVERY_HOURS` is set in `.env`, the catalog will auto-refresh in the background:
39
-
40
- ```dotenv
41
- SYNC_EVERY_HOURS=24 # Check every 24 hours
42
- ```
43
-
44
- ### ai_agent sync
45
-
46
- Manually synchronize the software catalog and rebuild the index.
47
-
48
- ```bash
49
- ai_agent sync
50
- ```
51
-
52
- **What it does**:
53
-
54
- 1. Loads the software catalog from `SOFTWARE_CATALOG` path
55
- 2. Embeds all tool descriptions using BGE-M3
56
- 3. Builds FAISS vector index
57
- 4. Saves artifacts to `artifacts/rag_index/`
58
-
59
- **When to use**:
60
-
61
- - After editing `catalog.jsonl`
62
- - After adding new tools
63
- - To force index rebuild
64
- - For testing catalog changes
65
-
66
- **Example**:
67
-
68
- ```bash
69
- $ ai_agent sync
70
- [sync] 150 → dataset/catalog.jsonl
71
- [sync] Embedding 150 tools... (5.2s)
72
- [sync] Building FAISS index...
73
- [sync] Saved to artifacts/rag_index/
74
- [sync] Sync complete.
75
- ```
76
-
77
- ## Command Aliases
78
-
79
- Both commands are available with either `ai_agent` or `ai-agent`:
80
-
81
- ```bash
82
- ai_agent chat # Works
83
- ai-agent chat # Also works
84
-
85
- ai_agent sync # Works
86
- ai-agent sync # Also works
87
- ```
88
-
89
- ## Common Usage Patterns
90
-
91
- ### Development Workflow
92
-
93
- ```bash
94
- # Edit catalog
95
- vim dataset/catalog.jsonl
96
-
97
- # Sync catalog
98
- ai_agent sync
99
-
100
- # Test changes
101
- ai_agent chat
102
- ```
103
-
104
- <!-- ### Production Deployment
105
-
106
- ```bash
107
- # In your deployment script:
108
- ai_agent sync # Ensure index is built
109
- nohup ai_agent chat & # Run in background
110
- ```
111
-
112
- Or use environment variable control:
113
-
114
- ```bash
115
- export SYNC_EVERY_HOURS=0 # Disable auto-refresh in production
116
- ai_agent chat
117
- ``` -->
118
-
119
- ### Testing & Development
120
-
121
- ```bash
122
- # Enable debug logging
123
- export LOGLEVEL_CONSOLE=DEBUG
124
- export LOG_PROMPTS=1
125
- ai_agent chat
126
- ```
127
-
128
- ## Environment Variables
129
-
130
- All configuration is via environment variables (see [Environment Variables Reference](environment.md)).
131
-
132
- ## Exit Codes
133
-
134
- - **0**: Success
135
- - **1**: General error (see logs)
136
-
137
- ## Troubleshooting
138
-
139
- ### Command Not Found
140
-
141
- If you see `command not found: ai_agent`:
142
-
143
- ```bash
144
- # Ensure package is installed
145
- pip install -e .
146
-
147
- # Check installation
148
- pip list | grep ai-agent
149
-
150
- # Try with python -m
151
- python -m ai_agent.cli chat
152
- ```
153
-
154
- ### Port Already in Use
155
-
156
- If port 7860 is occupied:
157
-
158
- ```bash
159
- # Find and kill process
160
- lsof -ti:7860 | xargs kill -9
161
-
162
- # Or change port in code (ui/app.py)
163
- ```
164
-
165
- ### Catalog Load Error
166
-
167
- If catalog fails to load:
168
-
169
- ```bash
170
- # Verify catalog exists
171
- ls -lh dataset/catalog.jsonl
172
-
173
- # Verify JSONL syntax
174
- python -c "import json; [json.loads(l) for l in open('dataset/catalog.jsonl')]"
175
-
176
- # Check environment variable
177
- echo $SOFTWARE_CATALOG
178
- ```
179
-
180
- ### Index Build Error
181
-
182
- If FAISS index building fails:
183
-
184
- ```bash
185
- # Check artifacts directory
186
- ls -lh artifacts/rag_index/
187
-
188
- # Rebuild manually
189
- rm -rf artifacts/rag_index/
190
- ai_agent sync
191
- ```
192
-
193
- ## Next Steps
194
-
195
- - Configure [Environment Variables](environment.md)
196
- - Review the [Changelog](changelog.md)
197
- - Return to [Getting Started](../getting-started/quickstart.md)
 
1
+ # CLI Commands
2
+
3
+ The AI Imaging Agent provides a command-line interface for starting the application and managing the software catalog.
4
+
5
+ ## Available Commands
6
+
7
+ ### ai_agent chat
8
+
9
+ Launch the chat-based user interface.
10
+
11
+ ```bash
12
+ ai_agent chat
13
+ ```
14
+
15
+ **What it does**:
16
+
17
+ 1. Performs startup catalog synchronization
18
+ 2. Loads the FAISS index
19
+ 3. Initializes the retrieval and agent pipelines
20
+ 4. Launches the Gradio web interface on `http://127.0.0.1:7860`
21
+ 5. Starts background catalog refresh (if configured)
22
+
23
+ **Options**: None (all configuration via `.env` and `config.yaml`)
24
+
25
+ **Example**:
26
+
27
+ ```bash
28
+ $ ai_agent chat
29
+ [startup-sync] 150 → dataset/catalog.jsonl
30
+ [startup-refresh] catalog unchanged; keeping existing FAISS index
31
+ Running on local URL: http://127.0.0.1:7860
32
+
33
+ To create a public link, set `share=True` in `launch()`.
34
+ ```
35
+
36
+ **Background Refresh**:
37
+
38
+ If `SYNC_EVERY_HOURS` is set in `.env`, the catalog will auto-refresh in the background:
39
+
40
+ ```dotenv
41
+ SYNC_EVERY_HOURS=24 # Check every 24 hours
42
+ ```
43
+
44
+ ### ai_agent sync
45
+
46
+ Manually synchronize the software catalog and rebuild the index.
47
+
48
+ ```bash
49
+ ai_agent sync
50
+ ```
51
+
52
+ **What it does**:
53
+
54
+ 1. Loads the software catalog from `SOFTWARE_CATALOG` path
55
+ 2. Embeds all tool descriptions using BGE-M3
56
+ 3. Builds FAISS vector index
57
+ 4. Saves artifacts to `artifacts/rag_index/`
58
+
59
+ **When to use**:
60
+
61
+ - After editing `catalog.jsonl`
62
+ - After adding new tools
63
+ - To force index rebuild
64
+ - For testing catalog changes
65
+
66
+ **Example**:
67
+
68
+ ```bash
69
+ $ ai_agent sync
70
+ [sync] 150 → dataset/catalog.jsonl
71
+ [sync] Embedding 150 tools... (5.2s)
72
+ [sync] Building FAISS index...
73
+ [sync] Saved to artifacts/rag_index/
74
+ [sync] Sync complete.
75
+ ```
76
+
77
+ ## Command Aliases
78
+
79
+ Both commands are available with either `ai_agent` or `ai-agent`:
80
+
81
+ ```bash
82
+ ai_agent chat # Works
83
+ ai-agent chat # Also works
84
+
85
+ ai_agent sync # Works
86
+ ai-agent sync # Also works
87
+ ```
88
+
89
+ ## Common Usage Patterns
90
+
91
+ ### Development Workflow
92
+
93
+ ```bash
94
+ # Edit catalog
95
+ vim dataset/catalog.jsonl
96
+
97
+ # Sync catalog
98
+ ai_agent sync
99
+
100
+ # Test changes
101
+ ai_agent chat
102
+ ```
103
+
104
+ <!-- ### Production Deployment
105
+
106
+ ```bash
107
+ # In your deployment script:
108
+ ai_agent sync # Ensure index is built
109
+ nohup ai_agent chat & # Run in background
110
+ ```
111
+
112
+ Or use environment variable control:
113
+
114
+ ```bash
115
+ export SYNC_EVERY_HOURS=0 # Disable auto-refresh in production
116
+ ai_agent chat
117
+ ``` -->
118
+
119
+ ### Testing & Development
120
+
121
+ ```bash
122
+ # Enable debug logging
123
+ export LOGLEVEL_CONSOLE=DEBUG
124
+ export LOG_PROMPTS=1
125
+ ai_agent chat
126
+ ```
127
+
128
+ ## Environment Variables
129
+
130
+ All configuration is via environment variables (see [Environment Variables Reference](environment.md)).
131
+
132
+ ## Exit Codes
133
+
134
+ - **0**: Success
135
+ - **1**: General error (see logs)
136
+
137
+ ## Troubleshooting
138
+
139
+ ### Command Not Found
140
+
141
+ If you see `command not found: ai_agent`:
142
+
143
+ ```bash
144
+ # Ensure package is installed
145
+ pip install -e .
146
+
147
+ # Check installation
148
+ pip list | grep ai-agent
149
+
150
+ # Try with python -m
151
+ python -m ai_agent.cli chat
152
+ ```
153
+
154
+ ### Port Already in Use
155
+
156
+ If port 7860 is occupied:
157
+
158
+ ```bash
159
+ # Find and kill process
160
+ lsof -ti:7860 | xargs kill -9
161
+
162
+ # Or change port in code (ui/app.py)
163
+ ```
164
+
165
+ ### Catalog Load Error
166
+
167
+ If catalog fails to load:
168
+
169
+ ```bash
170
+ # Verify catalog exists
171
+ ls -lh dataset/catalog.jsonl
172
+
173
+ # Verify JSONL syntax
174
+ python -c "import json; [json.loads(l) for l in open('dataset/catalog.jsonl')]"
175
+
176
+ # Check environment variable
177
+ echo $SOFTWARE_CATALOG
178
+ ```
179
+
180
+ ### Index Build Error
181
+
182
+ If FAISS index building fails:
183
+
184
+ ```bash
185
+ # Check artifacts directory
186
+ ls -lh artifacts/rag_index/
187
+
188
+ # Rebuild manually
189
+ rm -rf artifacts/rag_index/
190
+ ai_agent sync
191
+ ```
192
+
193
+ ## Next Steps
194
+
195
+ - Configure [Environment Variables](environment.md)
196
+ - Review the [Changelog](changelog.md)
197
+ - Return to [Getting Started](../getting-started/quickstart.md)
docs/reference/environment.md CHANGED
@@ -1,309 +1,309 @@
1
- # Environment Variables
2
-
3
- Configuration for the AI Imaging Agent is managed via environment variables, typically defined in a `.env` file.
4
-
5
- ## Required Variables
6
-
7
- ### OPENAI_API_KEY
8
-
9
- OpenAI API key for vision-language model calls.
10
-
11
- ```dotenv
12
- OPENAI_API_KEY=sk-xxxx
13
- ```
14
-
15
- **Where to get it**: [OpenAI API Keys](https://platform.openai.com/api-keys)
16
-
17
- **Required**: Yes (unless using alternative model provider)
18
-
19
- **Used by**: Agent VLM calls, tool selection
20
-
21
- ## Optional Variables
22
-
23
- ### SOFTWARE_CATALOG
24
-
25
- Path to the software catalog JSONL file.
26
-
27
- ```dotenv
28
- SOFTWARE_CATALOG=dataset/catalog.jsonl
29
- ```
30
-
31
- **Default**: `dataset/catalog.jsonl`
32
-
33
- **Required**: No (uses default)
34
-
35
- ### TOP_K
36
-
37
- Number of candidate tools to retrieve from FAISS search.
38
-
39
- ```dotenv
40
- TOP_K=8
41
- ```
42
-
43
- **Default**: `8`
44
-
45
- **Range**: 1-50 (recommended: 5-10)
46
-
47
- **Impact**: More candidates = better recall but slower VLM calls
48
-
49
- ### NUM_CHOICES
50
-
51
- Number of final tool recommendations to return to user.
52
-
53
- ```dotenv
54
- NUM_CHOICES=3
55
- ```
56
-
57
- **Default**: `3`
58
-
59
- **Range**: 1-10 (recommended: 3-5)
60
-
61
- **Impact**: Too many recommendations can overwhelm users
62
-
63
- ### GITHUB_TOKEN
64
-
65
- GitHub personal access token for repository info tool.
66
-
67
- ```dotenv
68
- GITHUB_TOKEN=ghp_xxxx
69
- ```
70
-
71
- **Where to get it**: [GitHub Tokens](https://github.com/settings/tokens)
72
-
73
- **Permissions needed**: `public_repo` (read access)
74
-
75
- **Required**: No (tool gracefully degrades without it)
76
-
77
- **Benefits**: Higher API rate limits, access to private repos
78
-
79
- ### SYNC_EVERY_HOURS
80
-
81
- Auto-refresh catalog interval in hours.
82
-
83
- ```dotenv
84
- SYNC_EVERY_HOURS=24
85
- ```
86
-
87
- **Default**: `0` (disabled)
88
-
89
- **Range**: 0 (disabled) or ≥1
90
-
91
- **Behavior**: Background thread checks catalog every N hours and rebuilds index if changed
92
-
93
- ## Logging Configuration
94
-
95
- ### LOGLEVEL_CONSOLE
96
-
97
- Console logging level.
98
-
99
- ```dotenv
100
- LOGLEVEL_CONSOLE=WARNING
101
- ```
102
-
103
- **Options**: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`
104
-
105
- **Default**: `WARNING`
106
-
107
- **Recommendation**:
108
- - Development: `DEBUG` or `INFO`
109
- - Production: `WARNING`
110
-
111
- ### LOGLEVEL_FILE
112
-
113
- File logging level.
114
-
115
- ```dotenv
116
- LOGLEVEL_FILE=INFO
117
- ```
118
-
119
- **Options**: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`
120
-
121
- **Default**: `INFO`
122
-
123
- **Files written to**: `logs/app_YYYYMMDD.log`
124
-
125
- ### FILE_LOG
126
-
127
- Enable file logging.
128
-
129
- ```dotenv
130
- FILE_LOG=1
131
- ```
132
-
133
- **Options**: `0` (disabled), `1` (enabled)
134
-
135
- **Default**: `1`
136
-
137
- ### LOG_DIR
138
-
139
- Directory for log files.
140
-
141
- ```dotenv
142
- LOG_DIR=logs
143
- ```
144
-
145
- **Default**: `logs`
146
-
147
- **Created automatically** if it doesn't exist
148
-
149
- ### LOG_PROMPTS
150
-
151
- Save VLM prompts and images for debugging.
152
-
153
- ```dotenv
154
- LOG_PROMPTS=1
155
- ```
156
-
157
- **Options**: `0` (disabled), `1` (enabled)
158
-
159
- **Default**: `0`
160
-
161
- **Writes to**: `logs/prompts/YYYYMMDD_HHMMSS/`
162
-
163
- **Contents**:
164
- - `prompt.txt`: Text prompt sent to VLM
165
- - `image_*.png`: Images included in prompt
166
- - `response.json`: VLM response
167
- - `metadata.json`: Request metadata
168
-
169
- **Warning**: Can consume significant disk space over time
170
-
171
- ## Model Configuration
172
-
173
- ### CONFIG_PATH
174
-
175
- Path to YAML model configuration file.
176
-
177
- ```dotenv
178
- CONFIG_PATH=config.yaml
179
- ```
180
-
181
- **Default**: `config.yaml`
182
-
183
- **See**: `config.yaml` for model configuration details
184
-
185
- ### Alternative Model Providers
186
-
187
- For custom OpenAI-compatible endpoints, configure in `config.yaml`:
188
-
189
- ```yaml
190
- agent_model:
191
- name: "model-name"
192
- base_url: "https://api.example.com/v1"
193
- api_key_env: "CUSTOM_API_KEY"
194
- ```
195
-
196
- Then in `.env`:
197
-
198
- ```dotenv
199
- CUSTOM_API_KEY=your-key-here
200
- ```
201
-
202
- <!-- ## Advanced Configuration
203
-
204
- ### RERANK_TOP_N
205
-
206
- Number of candidates to retrieve before reranking.
207
-
208
- ```dotenv
209
- RERANK_TOP_N=20
210
- ```
211
-
212
- **Default**: `20`
213
-
214
- **Interaction with TOP_K**:
215
- - FAISS retrieves `RERANK_TOP_N` candidates (e.g., 20)
216
- - CrossEncoder reranks them
217
- - Top `TOP_K` (e.g., 8) passed to VLM
218
-
219
- **Recommendation**: 2-3x `TOP_K` value
220
-
221
- -->
222
-
223
- ## .env File Example
224
-
225
- Complete example `.env` file:
226
-
227
- ```dotenv
228
- # Required
229
- OPENAI_API_KEY=sk-xxxx
230
-
231
- # Optional: Alternative providers
232
- EPFL_API_KEY=sk-xxxx
233
- GITHUB_TOKEN=ghp_xxxx
234
-
235
- # Catalog
236
- SOFTWARE_CATALOG=dataset/catalog.jsonl
237
- SYNC_EVERY_HOURS=24
238
-
239
- # Pipeline
240
- TOP_K=8
241
- NUM_CHOICES=3
242
-
243
- # Logging
244
- LOGLEVEL_CONSOLE=WARNING
245
- LOGLEVEL_FILE=INFO
246
- FILE_LOG=1
247
- LOG_DIR=logs
248
- LOG_PROMPTS=0 # Set to 1 for debugging
249
-
250
- # Model configuration
251
- CONFIG_PATH=config.yaml
252
- ```
253
-
254
- ## Loading Environment Variables
255
-
256
- ### Automatic Loading
257
-
258
- The application automatically loads `.env` from the repository root:
259
-
260
- ```python
261
- from dotenv import load_dotenv
262
- load_dotenv() # Loads .env automatically
263
- ```
264
-
265
- ### Manual Loading
266
-
267
- ```bash
268
- # Export manually
269
- export OPENAI_API_KEY=sk-xxxx
270
- export TOP_K=8
271
-
272
- # Or source .env
273
- set -a
274
- source .env
275
- set +a
276
- ```
277
-
278
- ### Docker
279
-
280
- Pass environment variables to Docker:
281
-
282
- ```bash
283
- docker run --env-file .env ai-agent
284
- ```
285
-
286
- ## Security Best Practices
287
-
288
- !!! warning "Never commit .env files"
289
- Add `.env` to `.gitignore` to prevent accidental commits
290
-
291
- !!! warning "Protect API keys"
292
- Treat API keys as sensitive credentials:
293
- - Never share in public repositories
294
- - Rotate keys if exposed
295
- - Use environment-specific keys (dev/prod)
296
-
297
- !!! tip "Use .env.example"
298
- Create `.env.example` with dummy values for documentation:
299
-
300
- ```dotenv
301
- OPENAI_API_KEY=sk-your-key-here
302
- GITHUB_TOKEN=ghp-your-token-here
303
- ```
304
-
305
- ## Next Steps
306
-
307
- - Review [CLI Commands](cli.md)
308
- - Check [Configuration Guide](../getting-started/configuration.md)
309
- - See [Changelog](changelog.md)
 
1
+ # Environment Variables
2
+
3
+ Configuration for the AI Imaging Agent is managed via environment variables, typically defined in a `.env` file.
4
+
5
+ ## Required Variables
6
+
7
+ ### OPENAI_API_KEY
8
+
9
+ OpenAI API key for vision-language model calls.
10
+
11
+ ```dotenv
12
+ OPENAI_API_KEY=sk-xxxx
13
+ ```
14
+
15
+ **Where to get it**: [OpenAI API Keys](https://platform.openai.com/api-keys)
16
+
17
+ **Required**: Yes (unless using alternative model provider)
18
+
19
+ **Used by**: Agent VLM calls, tool selection
20
+
21
+ ## Optional Variables
22
+
23
+ ### SOFTWARE_CATALOG
24
+
25
+ Path to the software catalog JSONL file.
26
+
27
+ ```dotenv
28
+ SOFTWARE_CATALOG=dataset/catalog.jsonl
29
+ ```
30
+
31
+ **Default**: `dataset/catalog.jsonl`
32
+
33
+ **Required**: No (uses default)
34
+
35
+ ### TOP_K
36
+
37
+ Number of candidate tools to retrieve from FAISS search.
38
+
39
+ ```dotenv
40
+ TOP_K=8
41
+ ```
42
+
43
+ **Default**: `8`
44
+
45
+ **Range**: 1-50 (recommended: 5-10)
46
+
47
+ **Impact**: More candidates = better recall but slower VLM calls
48
+
49
+ ### NUM_CHOICES
50
+
51
+ Number of final tool recommendations to return to user.
52
+
53
+ ```dotenv
54
+ NUM_CHOICES=3
55
+ ```
56
+
57
+ **Default**: `3`
58
+
59
+ **Range**: 1-10 (recommended: 3-5)
60
+
61
+ **Impact**: Too many recommendations can overwhelm users
62
+
63
+ ### GITHUB_TOKEN
64
+
65
+ GitHub personal access token for repository info tool.
66
+
67
+ ```dotenv
68
+ GITHUB_TOKEN=ghp_xxxx
69
+ ```
70
+
71
+ **Where to get it**: [GitHub Tokens](https://github.com/settings/tokens)
72
+
73
+ **Permissions needed**: `public_repo` (read access)
74
+
75
+ **Required**: No (tool gracefully degrades without it)
76
+
77
+ **Benefits**: Higher API rate limits, access to private repos
78
+
79
+ ### SYNC_EVERY_HOURS
80
+
81
+ Auto-refresh catalog interval in hours.
82
+
83
+ ```dotenv
84
+ SYNC_EVERY_HOURS=24
85
+ ```
86
+
87
+ **Default**: `0` (disabled)
88
+
89
+ **Range**: 0 (disabled) or ≥1
90
+
91
+ **Behavior**: Background thread checks catalog every N hours and rebuilds index if changed
92
+
93
+ ## Logging Configuration
94
+
95
+ ### LOGLEVEL_CONSOLE
96
+
97
+ Console logging level.
98
+
99
+ ```dotenv
100
+ LOGLEVEL_CONSOLE=WARNING
101
+ ```
102
+
103
+ **Options**: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`
104
+
105
+ **Default**: `WARNING`
106
+
107
+ **Recommendation**:
108
+ - Development: `DEBUG` or `INFO`
109
+ - Production: `WARNING`
110
+
111
+ ### LOGLEVEL_FILE
112
+
113
+ File logging level.
114
+
115
+ ```dotenv
116
+ LOGLEVEL_FILE=INFO
117
+ ```
118
+
119
+ **Options**: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`
120
+
121
+ **Default**: `INFO`
122
+
123
+ **Files written to**: `logs/app_YYYYMMDD.log`
124
+
125
+ ### FILE_LOG
126
+
127
+ Enable file logging.
128
+
129
+ ```dotenv
130
+ FILE_LOG=1
131
+ ```
132
+
133
+ **Options**: `0` (disabled), `1` (enabled)
134
+
135
+ **Default**: `1`
136
+
137
+ ### LOG_DIR
138
+
139
+ Directory for log files.
140
+
141
+ ```dotenv
142
+ LOG_DIR=logs
143
+ ```
144
+
145
+ **Default**: `logs`
146
+
147
+ **Created automatically** if it doesn't exist
148
+
149
+ ### LOG_PROMPTS
150
+
151
+ Save VLM prompts and images for debugging.
152
+
153
+ ```dotenv
154
+ LOG_PROMPTS=1
155
+ ```
156
+
157
+ **Options**: `0` (disabled), `1` (enabled)
158
+
159
+ **Default**: `0`
160
+
161
+ **Writes to**: `logs/prompts/YYYYMMDD_HHMMSS/`
162
+
163
+ **Contents**:
164
+ - `prompt.txt`: Text prompt sent to VLM
165
+ - `image_*.png`: Images included in prompt
166
+ - `response.json`: VLM response
167
+ - `metadata.json`: Request metadata
168
+
169
+ **Warning**: Can consume significant disk space over time
170
+
171
+ ## Model Configuration
172
+
173
+ ### CONFIG_PATH
174
+
175
+ Path to YAML model configuration file.
176
+
177
+ ```dotenv
178
+ CONFIG_PATH=config.yaml
179
+ ```
180
+
181
+ **Default**: `config.yaml`
182
+
183
+ **See**: `config.yaml` for model configuration details
184
+
185
+ ### Alternative Model Providers
186
+
187
+ For custom OpenAI-compatible endpoints, configure in `config.yaml`:
188
+
189
+ ```yaml
190
+ agent_model:
191
+ name: "model-name"
192
+ base_url: "https://api.example.com/v1"
193
+ api_key_env: "CUSTOM_API_KEY"
194
+ ```
195
+
196
+ Then in `.env`:
197
+
198
+ ```dotenv
199
+ CUSTOM_API_KEY=your-key-here
200
+ ```
201
+
202
+ <!-- ## Advanced Configuration
203
+
204
+ ### RERANK_TOP_N
205
+
206
+ Number of candidates to retrieve before reranking.
207
+
208
+ ```dotenv
209
+ RERANK_TOP_N=20
210
+ ```
211
+
212
+ **Default**: `20`
213
+
214
+ **Interaction with TOP_K**:
215
+ - FAISS retrieves `RERANK_TOP_N` candidates (e.g., 20)
216
+ - CrossEncoder reranks them
217
+ - Top `TOP_K` (e.g., 8) passed to VLM
218
+
219
+ **Recommendation**: 2-3x `TOP_K` value
220
+
221
+ -->
222
+
223
+ ## .env File Example
224
+
225
+ Complete example `.env` file:
226
+
227
+ ```dotenv
228
+ # Required
229
+ OPENAI_API_KEY=sk-xxxx
230
+
231
+ # Optional: Alternative providers
232
+ EPFL_API_KEY=sk-xxxx
233
+ GITHUB_TOKEN=ghp_xxxx
234
+
235
+ # Catalog
236
+ SOFTWARE_CATALOG=dataset/catalog.jsonl
237
+ SYNC_EVERY_HOURS=24
238
+
239
+ # Pipeline
240
+ TOP_K=8
241
+ NUM_CHOICES=3
242
+
243
+ # Logging
244
+ LOGLEVEL_CONSOLE=WARNING
245
+ LOGLEVEL_FILE=INFO
246
+ FILE_LOG=1
247
+ LOG_DIR=logs
248
+ LOG_PROMPTS=0 # Set to 1 for debugging
249
+
250
+ # Model configuration
251
+ CONFIG_PATH=config.yaml
252
+ ```
253
+
254
+ ## Loading Environment Variables
255
+
256
+ ### Automatic Loading
257
+
258
+ The application automatically loads `.env` from the repository root:
259
+
260
+ ```python
261
+ from dotenv import load_dotenv
262
+ load_dotenv() # Loads .env automatically
263
+ ```
264
+
265
+ ### Manual Loading
266
+
267
+ ```bash
268
+ # Export manually
269
+ export OPENAI_API_KEY=sk-xxxx
270
+ export TOP_K=8
271
+
272
+ # Or source .env
273
+ set -a
274
+ source .env
275
+ set +a
276
+ ```
277
+
278
+ ### Docker
279
+
280
+ Pass environment variables to Docker:
281
+
282
+ ```bash
283
+ docker run --env-file .env ai-agent
284
+ ```
285
+
286
+ ## Security Best Practices
287
+
288
+ !!! warning "Never commit .env files"
289
+ Add `.env` to `.gitignore` to prevent accidental commits
290
+
291
+ !!! warning "Protect API keys"
292
+ Treat API keys as sensitive credentials:
293
+ - Never share in public repositories
294
+ - Rotate keys if exposed
295
+ - Use environment-specific keys (dev/prod)
296
+
297
+ !!! tip "Use .env.example"
298
+ Create `.env.example` with dummy values for documentation:
299
+
300
+ ```dotenv
301
+ OPENAI_API_KEY=sk-your-key-here
302
+ GITHUB_TOKEN=ghp-your-token-here
303
+ ```
304
+
305
+ ## Next Steps
306
+
307
+ - Review [CLI Commands](cli.md)
308
+ - Check [Configuration Guide](../getting-started/configuration.md)
309
+ - See [Changelog](changelog.md)
docs/user-guide/advanced-features.md CHANGED
@@ -1,387 +1,387 @@
1
- # Advanced Features (not tested for now..)
2
-
3
- The AI Imaging Agent includes several advanced features for power users and specialized use cases.
4
-
5
- ## Control Tags
6
-
7
- Control tags modify agent behavior using special syntax in your queries.
8
-
9
- ### Exclude Tools
10
-
11
- Filter out specific tools from results:
12
-
13
- ```
14
- Find lung segmentation tools [EXCLUDE:totalsegmentator|medicalsam]
15
- ```
16
-
17
- **Syntax**: `[EXCLUDE:tool1|tool2|tool3]`
18
-
19
- **Use cases**:
20
-
21
- - You've already tried certain tools
22
- - Exclude tools you don't have access to
23
- - Filter by licensing (exclude proprietary tools)
24
- - Remove tools with specific limitations
25
-
26
- **Example**:
27
- ```
28
- You: Segment kidneys [EXCLUDE:totalsegmentator]
29
- Agent: [Returns kidney segmentation tools except TotalSegmentator]
30
-
31
- You: Find open-source options [EXCLUDE:proprietarytool1|proprietarytool2]
32
- Agent: [Returns only open-source tools]
33
- ```
34
-
35
- ### Notes
36
-
37
- Only `[EXCLUDE:...]` is currently interpreted as a control tag for retrieval filtering.
38
-
39
- ## Alternative Searches
40
-
41
- Request the agent to search with different strategies.
42
-
43
- ### Requesting Alternatives
44
-
45
- Use natural language:
46
-
47
- ```
48
- Can you search for alternatives?
49
- Show me other options
50
- Find different tools
51
- What else is available?
52
- ```
53
-
54
- **What happens**:
55
-
56
- - Agent formulates alternative query
57
- - Uses different phrasing/keywords for broader coverage
58
- - Searches with different emphasis
59
- - Returns new set of recommendations
60
-
61
- **Limit**: Up to 3 alternative searches per conversation
62
-
63
- ### When to Use
64
-
65
- - Initial results don't quite match
66
- - Want to see different approaches
67
- - Exploring the catalog
68
- - Looking for specialized tools
69
-
70
- **Example conversation**:
71
- ```
72
- You: Segment lungs from this CT
73
- Agent: [Provides general lung segmentation tools]
74
-
75
- You: Can you search for alternatives?
76
- Agent: [Searches with emphasis on "airway segmentation", "pulmonary analysis"]
77
-
78
- You: Show me other options
79
- Agent: [Searches with emphasis on "CT thorax processing", "respiratory imaging"]
80
- ```
81
-
82
- ## Multi-Model Support
83
-
84
- ### Selecting Different Models
85
-
86
- The UI provides a model selector dropdown:
87
-
88
- Available models (configurable in `config.yaml`):
89
-
90
- - **gpt-4o-mini**: Faster, lower cost
91
- - **gpt-4o**: Higher accuracy, multimodal
92
- - **gpt-5.1**: Latest capabilities (if available)
93
- - **Custom endpoints**: EPFL, local servers, etc.
94
-
95
- ### Model Trade-offs
96
-
97
- | Model | Speed | Cost | Accuracy | Vision |
98
- |-------|-------|------|----------|--------|
99
- | gpt-4o-mini | ⚡⚡⚡ | 💰 | ⭐⭐⭐ | ✅ |
100
- | gpt-4o | ⚡⚡ | 💰💰 | ⭐⭐⭐⭐ | ✅✅ |
101
- | gpt-5.1 | ⚡ | 💰💰💰 | ⭐⭐⭐⭐⭐ | ✅✅✅ |
102
-
103
- ### When to Switch Models
104
-
105
- **Use gpt-4o-mini when**:
106
-
107
- - Doing quick explorations
108
- - Cost is a concern
109
- - Tasks are straightforward
110
- - Query is well-specified
111
-
112
- **Use gpt-4o when**:
113
-
114
- - Complex visual analysis needed
115
- - Accuracy is critical
116
- - Ambiguous queries
117
- - Multi-step reasoning required
118
-
119
- **Use gpt-5.1 when**:
120
-
121
- - Maximum accuracy needed
122
- - Complex multi-modal tasks
123
- - Research/publication work
124
-
125
- ## Repository Info Tool
126
-
127
- ### What It Does
128
-
129
- The agent can fetch detailed information about GitHub repositories:
130
-
131
- ```
132
- You: Tell me about TotalSegmentator
133
- Agent: [Fetches repo info from GitHub via DeepWiki or repocards]
134
-
135
- Repository: wasserth/TotalSegmentator
136
- Description: Automated multi-organ segmentation in CT and MR images
137
- Stars: 1.2k
138
- Language: Python
139
- Topics: segmentation, medical-imaging, deep-learning
140
- Last Updated: 2024-03-15
141
- License: Apache-2.0
142
- ```
143
-
144
- ### Data Sources
145
-
146
- 1. **DeepWiki MCP** (primary): Fast, pre-indexed repository documentation
147
- 2. **Repocards** (fallback): Direct library-based fetch
148
-
149
- ### Usage
150
-
151
- Ask about tools naturally:
152
-
153
- ```
154
- What is [tool name]?
155
- Tell me more about [repository]
156
- Show me details for [tool]
157
- ```
158
-
159
- ## Conversation State Management
160
-
161
- ### State Tracking
162
-
163
- The agent maintains state across conversation:
164
-
165
- - **Uploaded files**: All files in session
166
- - **Preview images**: Converted images for VLM
167
- - **Excluded tools**: Tools filtered via `[EXCLUDE:]`
168
- - **Conversation history**: Previous messages and context
169
- - **Turn counter**: Current conversation turn
170
-
171
- ### Viewing State
172
-
173
- In the sidebar (debug mode):
174
-
175
- ```json
176
- {
177
- "conversation_turn": 3,
178
- "uploaded_files": ["scan.dcm", "brain.nii"],
179
- "excluded_tools": ["tool1", "tool2"],
180
- "preview_images": ["/tmp/scan_preview.png"]
181
- }
182
- ```
183
-
184
- ### Resetting State
185
-
186
- To start fresh:
187
- - Refresh the page
188
- - Clear uploaded files
189
- - Start new conversation
190
-
191
- ## Retrieval Query Behavior
192
-
193
- ### How It Works
194
-
195
- The retrieval pipeline currently does not add semantic neighbor terms.
196
- Instead, it builds retrieval queries from:
197
-
198
- ```
199
- User text: "segment brain"
200
- + format hints from files: format:DICOM / format:NIfTI
201
- + compact image metadata: modality, anatomy, dimensions (when available)
202
- ```
203
-
204
- **Based on**:
205
-
206
- - BGE-M3 embeddings
207
- - Format-aware hinting from uploaded files
208
- - Metadata-aware context from image inspection
209
-
210
- ### Benefits
211
-
212
- - ✅ Stronger format compatibility matching
213
- - ✅ Better ranking for modality/dimension-specific tasks
214
- - ✅ More predictable retrieval behavior
215
-
216
- ### Customization
217
-
218
- If initial results are too sparse, the pipeline retries with a broader query
219
- formulation automatically.
220
-
221
- ## Format-Aware Matching
222
-
223
- ### Input Format Tokens
224
-
225
- File uploads add format tokens to queries:
226
-
227
- ```
228
- Uploaded: scan.dcm (DICOM)
229
- Query enhancement: "segment lungs format:DICOM format:CT format:3D"
230
- ```
231
-
232
- ### How It Helps
233
-
234
- - **Narrows results**: Shows compatible tools first
235
- - **Boosts relevance**: DICOM tools rank higher for DICOM
236
- - **Compatibility check**: Agent verifies format support
237
-
238
- ### Supported Formats
239
-
240
- Tokens added for:
241
- - File extension (`.dcm`, `.nii`, `.png`)
242
- - Detected format (DICOM, NIfTI, TIFF)
243
- - Modality for medical images (CT, MRI, XR)
244
- - Dimensions (2D, 3D, 4D)
245
-
246
- ## Iterative Retrieval
247
-
248
- ### Auto-Retry on Low Results
249
-
250
- If initial search returns <5 candidates:
251
-
252
- 1. **Retry #1**: Alternative query with semantic expansion
253
- 2. **Retry #2**: Further expansion with broader terms
254
- 3. **Max 2 retries**: Then return best available
255
-
256
- ### Why It Matters
257
-
258
- - Handles rare/specialized queries
259
- - Finds tools even with limited matches
260
- - Automatic - no user action needed
261
-
262
- ### Example
263
-
264
- ```
265
- Query: "segment rare anatomical structure"
266
- Initial: 2 candidates found
267
- Retry 1: Expanded to "segment anatomy structure region organ"
268
- Result: 7 candidates found ✓
269
- ```
270
-
271
- ## Debug Features
272
-
273
- ### Prompt Logging
274
-
275
- Enable in `.env`:
276
-
277
- ```dotenv
278
- LOG_PROMPTS=1
279
- ```
280
-
281
- **Saves**:
282
-
283
- - VLM prompts sent to API
284
- - Images included in prompts
285
- - Response JSON
286
- - Timestamp and metadata
287
-
288
- **Location**: `logs/prompts/YYYYMMDD_HHMMSS/`
289
-
290
- **Contents**:
291
- ```
292
- logs/prompts/20240315_143022/
293
- ├── prompt.txt # Text prompt
294
- ├── image_0.png # Uploaded image
295
- ├── response.json # API response
296
- └── metadata.json # Request metadata
297
- ```
298
-
299
- ### Execution Traces
300
-
301
- Always shown in chat (expandable):
302
-
303
- ```html
304
- <details>
305
- <summary>🔧 Execution Trace</summary>
306
- ...detailed logs...
307
- </details>
308
- ```
309
-
310
- Shows:
311
-
312
- - Tool calls made
313
- - Parameters used
314
- - API responses
315
- - Timing information
316
-
317
- ## Catalog Synchronization
318
-
319
- ### Auto-Refresh
320
-
321
- Configured via `.env`:
322
-
323
- ```dotenv
324
- SYNC_EVERY_HOURS=24
325
- ```
326
-
327
- **Behavior**:
328
-
329
- - Background thread checks for catalog updates
330
- - Reloads FAISS index if changed
331
- - No UI interruption
332
- - Logs refresh activity
333
-
334
- ### Manual Sync
335
-
336
- Force synchronization:
337
-
338
- ```bash
339
- ai_agent sync
340
- ```
341
-
342
- Updates:
343
-
344
- - Software catalog
345
- - Embeddings
346
- - FAISS index
347
- - Vocabulary for expansion
348
-
349
- ## Advanced Configuration
350
-
351
- ### Custom Catalog
352
-
353
- Use your own tool catalog:
354
-
355
- ```dotenv
356
- SOFTWARE_CATALOG=/path/to/custom_catalog.jsonl
357
- ```
358
-
359
- **Format**: JSONL with schema.org SoftwareSourceCode
360
-
361
- ### API Endpoints
362
-
363
- Configure custom OpenAI-compatible endpoints in `config.yaml`:
364
-
365
- ```yaml
366
- available_models:
367
- - display_name: "Local LLM"
368
- name: "llama-3.1"
369
- base_url: "http://localhost:8000/v1"
370
- api_key_env: "LOCAL_API_KEY"
371
- ```
372
-
373
- ### Pipeline Parameters
374
-
375
- Fine-tune retrieval:
376
-
377
- ```dotenv
378
- TOP_K=8 # Candidates to retrieve
379
- NUM_CHOICES=3 # Final recommendations
380
- RERANK_TOP_N=20 # Candidates before reranking
381
- ```
382
-
383
- ## Next Steps
384
-
385
- - Dive into [Architecture Overview](../architecture/overview.md)
386
- - Learn about [Development and Contributing](../development/contributing.md)
387
- - Check [Environment Variables Reference](../reference/environment.md)
 
1
+ # Advanced Features (not tested for now..)
2
+
3
+ The AI Imaging Agent includes several advanced features for power users and specialized use cases.
4
+
5
+ ## Control Tags
6
+
7
+ Control tags modify agent behavior using special syntax in your queries.
8
+
9
+ ### Exclude Tools
10
+
11
+ Filter out specific tools from results:
12
+
13
+ ```
14
+ Find lung segmentation tools [EXCLUDE:totalsegmentator|medicalsam]
15
+ ```
16
+
17
+ **Syntax**: `[EXCLUDE:tool1|tool2|tool3]`
18
+
19
+ **Use cases**:
20
+
21
+ - You've already tried certain tools
22
+ - Exclude tools you don't have access to
23
+ - Filter by licensing (exclude proprietary tools)
24
+ - Remove tools with specific limitations
25
+
26
+ **Example**:
27
+ ```
28
+ You: Segment kidneys [EXCLUDE:totalsegmentator]
29
+ Agent: [Returns kidney segmentation tools except TotalSegmentator]
30
+
31
+ You: Find open-source options [EXCLUDE:proprietarytool1|proprietarytool2]
32
+ Agent: [Returns only open-source tools]
33
+ ```
34
+
35
+ ### Notes
36
+
37
+ Only `[EXCLUDE:...]` is currently interpreted as a control tag for retrieval filtering.
38
+
39
+ ## Alternative Searches
40
+
41
+ Request the agent to search with different strategies.
42
+
43
+ ### Requesting Alternatives
44
+
45
+ Use natural language:
46
+
47
+ ```
48
+ Can you search for alternatives?
49
+ Show me other options
50
+ Find different tools
51
+ What else is available?
52
+ ```
53
+
54
+ **What happens**:
55
+
56
+ - Agent formulates alternative query
57
+ - Uses different phrasing/keywords for broader coverage
58
+ - Searches with different emphasis
59
+ - Returns new set of recommendations
60
+
61
+ **Limit**: Up to 3 alternative searches per conversation
62
+
63
+ ### When to Use
64
+
65
+ - Initial results don't quite match
66
+ - Want to see different approaches
67
+ - Exploring the catalog
68
+ - Looking for specialized tools
69
+
70
+ **Example conversation**:
71
+ ```
72
+ You: Segment lungs from this CT
73
+ Agent: [Provides general lung segmentation tools]
74
+
75
+ You: Can you search for alternatives?
76
+ Agent: [Searches with emphasis on "airway segmentation", "pulmonary analysis"]
77
+
78
+ You: Show me other options
79
+ Agent: [Searches with emphasis on "CT thorax processing", "respiratory imaging"]
80
+ ```
81
+
82
+ ## Multi-Model Support
83
+
84
+ ### Selecting Different Models
85
+
86
+ The UI provides a model selector dropdown:
87
+
88
+ Available models (configurable in `config.yaml`):
89
+
90
+ - **gpt-4o-mini**: Faster, lower cost
91
+ - **gpt-4o**: Higher accuracy, multimodal
92
+ - **gpt-5.1**: Latest capabilities (if available)
93
+ - **Custom endpoints**: EPFL, local servers, etc.
94
+
95
+ ### Model Trade-offs
96
+
97
+ | Model | Speed | Cost | Accuracy | Vision |
98
+ |-------|-------|------|----------|--------|
99
+ | gpt-4o-mini | ⚡⚡⚡ | 💰 | ⭐⭐⭐ | ✅ |
100
+ | gpt-4o | ⚡⚡ | 💰💰 | ⭐⭐⭐⭐ | ✅✅ |
101
+ | gpt-5.1 | ⚡ | 💰💰💰 | ⭐⭐⭐⭐⭐ | ✅✅✅ |
102
+
103
+ ### When to Switch Models
104
+
105
+ **Use gpt-4o-mini when**:
106
+
107
+ - Doing quick explorations
108
+ - Cost is a concern
109
+ - Tasks are straightforward
110
+ - Query is well-specified
111
+
112
+ **Use gpt-4o when**:
113
+
114
+ - Complex visual analysis needed
115
+ - Accuracy is critical
116
+ - Ambiguous queries
117
+ - Multi-step reasoning required
118
+
119
+ **Use gpt-5.1 when**:
120
+
121
+ - Maximum accuracy needed
122
+ - Complex multi-modal tasks
123
+ - Research/publication work
124
+
125
+ ## Repository Info Tool
126
+
127
+ ### What It Does
128
+
129
+ The agent can fetch detailed information about GitHub repositories:
130
+
131
+ ```
132
+ You: Tell me about TotalSegmentator
133
+ Agent: [Fetches repo info from GitHub via DeepWiki or repocards]
134
+
135
+ Repository: wasserth/TotalSegmentator
136
+ Description: Automated multi-organ segmentation in CT and MR images
137
+ Stars: 1.2k
138
+ Language: Python
139
+ Topics: segmentation, medical-imaging, deep-learning
140
+ Last Updated: 2024-03-15
141
+ License: Apache-2.0
142
+ ```
143
+
144
+ ### Data Sources
145
+
146
+ 1. **DeepWiki MCP** (primary): Fast, pre-indexed repository documentation
147
+ 2. **Repocards** (fallback): Direct library-based fetch
148
+
149
+ ### Usage
150
+
151
+ Ask about tools naturally:
152
+
153
+ ```
154
+ What is [tool name]?
155
+ Tell me more about [repository]
156
+ Show me details for [tool]
157
+ ```
158
+
159
+ ## Conversation State Management
160
+
161
+ ### State Tracking
162
+
163
+ The agent maintains state across conversation:
164
+
165
+ - **Uploaded files**: All files in session
166
+ - **Preview images**: Converted images for VLM
167
+ - **Excluded tools**: Tools filtered via `[EXCLUDE:]`
168
+ - **Conversation history**: Previous messages and context
169
+ - **Turn counter**: Current conversation turn
170
+
171
+ ### Viewing State
172
+
173
+ In the sidebar (debug mode):
174
+
175
+ ```json
176
+ {
177
+ "conversation_turn": 3,
178
+ "uploaded_files": ["scan.dcm", "brain.nii"],
179
+ "excluded_tools": ["tool1", "tool2"],
180
+ "preview_images": ["/tmp/scan_preview.png"]
181
+ }
182
+ ```
183
+
184
+ ### Resetting State
185
+
186
+ To start fresh:
187
+ - Refresh the page
188
+ - Clear uploaded files
189
+ - Start new conversation
190
+
191
+ ## Retrieval Query Behavior
192
+
193
+ ### How It Works
194
+
195
+ The retrieval pipeline currently does not add semantic neighbor terms.
196
+ Instead, it builds retrieval queries from:
197
+
198
+ ```
199
+ User text: "segment brain"
200
+ + format hints from files: format:DICOM / format:NIfTI
201
+ + compact image metadata: modality, anatomy, dimensions (when available)
202
+ ```
203
+
204
+ **Based on**:
205
+
206
+ - BGE-M3 embeddings
207
+ - Format-aware hinting from uploaded files
208
+ - Metadata-aware context from image inspection
209
+
210
+ ### Benefits
211
+
212
+ - ✅ Stronger format compatibility matching
213
+ - ✅ Better ranking for modality/dimension-specific tasks
214
+ - ✅ More predictable retrieval behavior
215
+
216
+ ### Customization
217
+
218
+ If initial results are too sparse, the pipeline retries with a broader query
219
+ formulation automatically.
220
+
221
+ ## Format-Aware Matching
222
+
223
+ ### Input Format Tokens
224
+
225
+ File uploads add format tokens to queries:
226
+
227
+ ```
228
+ Uploaded: scan.dcm (DICOM)
229
+ Query enhancement: "segment lungs format:DICOM format:CT format:3D"
230
+ ```
231
+
232
+ ### How It Helps
233
+
234
+ - **Narrows results**: Shows compatible tools first
235
+ - **Boosts relevance**: DICOM tools rank higher for DICOM
236
+ - **Compatibility check**: Agent verifies format support
237
+
238
+ ### Supported Formats
239
+
240
+ Tokens added for:
241
+ - File extension (`.dcm`, `.nii`, `.png`)
242
+ - Detected format (DICOM, NIfTI, TIFF)
243
+ - Modality for medical images (CT, MRI, XR)
244
+ - Dimensions (2D, 3D, 4D)
245
+
246
+ ## Iterative Retrieval
247
+
248
+ ### Auto-Retry on Low Results
249
+
250
+ If initial search returns <5 candidates:
251
+
252
+ 1. **Retry #1**: Alternative query with semantic expansion
253
+ 2. **Retry #2**: Further expansion with broader terms
254
+ 3. **Max 2 retries**: Then return best available
255
+
256
+ ### Why It Matters
257
+
258
+ - Handles rare/specialized queries
259
+ - Finds tools even with limited matches
260
+ - Automatic - no user action needed
261
+
262
+ ### Example
263
+
264
+ ```
265
+ Query: "segment rare anatomical structure"
266
+ Initial: 2 candidates found
267
+ Retry 1: Expanded to "segment anatomy structure region organ"
268
+ Result: 7 candidates found ✓
269
+ ```
270
+
271
+ ## Debug Features
272
+
273
+ ### Prompt Logging
274
+
275
+ Enable in `.env`:
276
+
277
+ ```dotenv
278
+ LOG_PROMPTS=1
279
+ ```
280
+
281
+ **Saves**:
282
+
283
+ - VLM prompts sent to API
284
+ - Images included in prompts
285
+ - Response JSON
286
+ - Timestamp and metadata
287
+
288
+ **Location**: `logs/prompts/YYYYMMDD_HHMMSS/`
289
+
290
+ **Contents**:
291
+ ```
292
+ logs/prompts/20240315_143022/
293
+ ├── prompt.txt # Text prompt
294
+ ├── image_0.png # Uploaded image
295
+ ├── response.json # API response
296
+ └── metadata.json # Request metadata
297
+ ```
298
+
299
+ ### Execution Traces
300
+
301
+ Always shown in chat (expandable):
302
+
303
+ ```html
304
+ <details>
305
+ <summary>🔧 Execution Trace</summary>
306
+ ...detailed logs...
307
+ </details>
308
+ ```
309
+
310
+ Shows:
311
+
312
+ - Tool calls made
313
+ - Parameters used
314
+ - API responses
315
+ - Timing information
316
+
317
+ ## Catalog Synchronization
318
+
319
+ ### Auto-Refresh
320
+
321
+ Configured via `.env`:
322
+
323
+ ```dotenv
324
+ SYNC_EVERY_HOURS=24
325
+ ```
326
+
327
+ **Behavior**:
328
+
329
+ - Background thread checks for catalog updates
330
+ - Reloads FAISS index if changed
331
+ - No UI interruption
332
+ - Logs refresh activity
333
+
334
+ ### Manual Sync
335
+
336
+ Force synchronization:
337
+
338
+ ```bash
339
+ ai_agent sync
340
+ ```
341
+
342
+ Updates:
343
+
344
+ - Software catalog
345
+ - Embeddings
346
+ - FAISS index
347
+ - Vocabulary for expansion
348
+
349
+ ## Advanced Configuration
350
+
351
+ ### Custom Catalog
352
+
353
+ Use your own tool catalog:
354
+
355
+ ```dotenv
356
+ SOFTWARE_CATALOG=/path/to/custom_catalog.jsonl
357
+ ```
358
+
359
+ **Format**: JSONL with schema.org SoftwareSourceCode
360
+
361
+ ### API Endpoints
362
+
363
+ Configure custom OpenAI-compatible endpoints in `config.yaml`:
364
+
365
+ ```yaml
366
+ available_models:
367
+ - display_name: "Local LLM"
368
+ name: "llama-3.1"
369
+ base_url: "http://localhost:8000/v1"
370
+ api_key_env: "LOCAL_API_KEY"
371
+ ```
372
+
373
+ ### Pipeline Parameters
374
+
375
+ Fine-tune retrieval:
376
+
377
+ ```dotenv
378
+ TOP_K=8 # Candidates to retrieve
379
+ NUM_CHOICES=3 # Final recommendations
380
+ RERANK_TOP_N=20 # Candidates before reranking
381
+ ```
382
+
383
+ ## Next Steps
384
+
385
+ - Dive into [Architecture Overview](../architecture/overview.md)
386
+ - Learn about [Development and Contributing](../development/contributing.md)
387
+ - Check [Environment Variables Reference](../reference/environment.md)
docs/user-guide/chat-interface.md CHANGED
@@ -1,275 +1,275 @@
1
- # Using the Chat Interface
2
-
3
- The AI Imaging Agent provides a conversational interface for discovering and using imaging software. This guide explains how to interact with the chat interface effectively.
4
-
5
- ## Interface Layout
6
-
7
- The interface consists of three main areas:
8
-
9
- ### Left Panel: Chat Conversation
10
- - **Message History**: Your conversation with the agent
11
- - **Rich Media Rendering**: Images, tool cards, and files are displayed inline
12
- - **Input Box**: Type your messages at the bottom
13
- - **File Upload**: Attach files via the paperclip icon or drag-and-drop
14
-
15
- ### Right Panel: Sidebar
16
- - **Files Tab**: View uploaded files with format information
17
- - **State Tab**: Debug information showing conversation state
18
-
19
- ### Header
20
- - **Model Selector**: Choose which AI model to use
21
- - **Settings**: Access configuration options
22
-
23
- ## Basic Workflow
24
-
25
- ### 1. Upload Files
26
-
27
- Upload images or other files in several ways:
28
-
29
- - **Drag and Drop**: Drag files directly onto the upload area
30
- - **Click to Browse**: Click the upload area to select files
31
- - **Attach to Message**: Use the paperclip icon in the input box
32
-
33
- Files are automatically processed and metadata is extracted.
34
-
35
- ### 2. Describe Your Task
36
-
37
- Use natural language to describe what you want to do:
38
-
39
- !!! example "Good Task Descriptions"
40
- - "I want to segment the lungs from this CT scan"
41
- - "Help me detect tumors in this MRI"
42
- - "I need to register these two brain images"
43
- - "Extract text from this medical report"
44
- - "Classify the organ shown in this ultrasound"
45
-
46
- ### 3. Review Recommendations
47
-
48
- The agent returns ranked tool recommendations with:
49
-
50
- - **Tool Cards**: Each tool is presented in a card format
51
- - **Accuracy Scores**: Confidence levels for each recommendation
52
- - **Explanations**: Why each tool matches your request
53
- - **Metadata**: Technical details about compatibility
54
-
55
- ### 4. Run Demos (Optional)
56
-
57
- The agent may offer to run demos:
58
-
59
- ```
60
- Agent: Would you like me to run the demo with your image?
61
- ```
62
-
63
- Respond with affirmative language:
64
- - "yes"
65
- - "sure"
66
- - "ok"
67
- - "please"
68
- - "go ahead"
69
-
70
- The agent will execute the tool and show results.
71
-
72
- ## Multi-Turn Conversations
73
-
74
- The agent maintains context across multiple messages:
75
-
76
- !!! example "Multi-Turn Example"
77
- ```
78
- You: I have a lung CT scan [uploads file]
79
-
80
- Agent: I can see you have a DICOM CT image. What would you like to do with it?
81
-
82
- You: Segment the airways
83
-
84
- Agent: [Provides airway segmentation tool recommendations]
85
-
86
- You: What about segmenting the whole lung?
87
-
88
- Agent: [Provides lung segmentation tools, remembering you're working with CT]
89
-
90
- You: Show me alternatives
91
-
92
- Agent: [Provides additional options]
93
- ```
94
-
95
- ## Advanced Features
96
-
97
- ### Excluding Tools
98
-
99
- Exclude specific tools using the `[EXCLUDE:...]` tag:
100
-
101
- ```
102
- Find segmentation tools [EXCLUDE:totalsegmentator|medicalsam]
103
- ```
104
-
105
- You can exclude multiple tools separated by `|`.
106
-
107
- ### Requesting Alternatives
108
-
109
- Ask the agent to search with different strategies:
110
-
111
- ```
112
- Can you search for alternatives?
113
-
114
- Show me other options
115
-
116
- Find different tools for this task
117
- ```
118
-
119
- The agent can perform up to 3 alternative searches per conversation.
120
-
121
- ## Understanding Agent Responses
122
-
123
- ### Recommendation Cards
124
-
125
- Each recommendation includes:
126
-
127
- #### Header
128
- - **Rank Number**: 1, 2, 3 (1 = best match)
129
- - **Tool Name**: Software/tool identifier
130
- - **Accuracy Score**: 0-100% confidence
131
-
132
- #### Body
133
- - **Description**: What the tool does
134
- - **Explanation**: Why it matches your task
135
- - **Demo Link**: Click to visit runnable example
136
-
137
- #### Footer Metadata
138
- - **Modalities**: CT, MRI, X-ray, etc.
139
- - **Dimensions**: 2D, 3D, 4D
140
- - **Formats**: Supported file formats (DICOM, NIfTI, etc.)
141
- - **License**: Software license information
142
- - **Tags**: Categorization and keywords
143
-
144
- ### Execution Traces
145
-
146
- When demos run, you'll see execution details:
147
-
148
- ```
149
- <details>
150
- <summary>Tool Execution Trace</summary>
151
-
152
- Image uploaded to Gradio Space
153
- Processing started...
154
- Result: Success
155
- Output saved to: result.png
156
- </details>
157
- ```
158
-
159
- Click to expand and see full execution logs.
160
-
161
- ### Clarification Questions
162
-
163
- Sometimes the agent needs more information:
164
-
165
- ```
166
- Agent: I found several segmentation tools. Which organ are you trying to segment?
167
-
168
- You: The liver
169
-
170
- Agent: [Provides liver-specific segmentation tools]
171
- ```
172
-
173
- ## File Management
174
-
175
- ### Uploaded Files List
176
-
177
- The sidebar shows all uploaded files with:
178
-
179
- - **Filename**: Original file name
180
- - **Format**: File type/extension
181
- - **Size**: File size
182
- - **Preview**: Thumbnail (for images)
183
-
184
- ### Image Previews
185
-
186
- Medical images are automatically converted:
187
-
188
- - **DICOM**: PNG previews; 3D series use orthogonal composite views (MIPs + central slices) rather than a single slice
189
- - **NIfTI**: PNG previews built from orthogonal composite views of the volume
190
- - **TIFF Stacks**: PNG previews built from orthogonal composite views of the stack
191
- - **Standard 2D Images**: Resized PNG preview of the original image
192
-
193
- Previews are used for VLM analysis while preserving original format metadata.
194
-
195
- ### Removing Files
196
-
197
- Click the 'X' button next to a file to remove it from the current session.
198
-
199
- ## Conversation State
200
-
201
- The debug sidebar shows:
202
-
203
- ### Current State
204
- - **Status**: idle, processing, waiting
205
- - **Conversation Turn**: Current turn number
206
- - **Excluded Tools**: Tools filtered from results
207
-
208
- ### Preview Images
209
- - Images prepared for VLM analysis
210
- - Format conversions applied
211
-
212
- ## Tips for Effective Interaction
213
-
214
- !!! tip "Be Specific About Requirements"
215
- Mention specific needs:
216
-
217
- - "I need a tool that works with NIfTI files"
218
- - "Must support 3D volumes"
219
- - "Looking for open-source options"
220
-
221
- !!! tip "Use Conversational Language"
222
- Natural language works best:
223
-
224
- - ✅ "Help me find tool that segments kidneys"
225
- - ❌ "kidney_segmentation_tool filter:3D"
226
-
227
- !!! tip "Iterate Based on Results"
228
- If initial results aren't perfect, refine:
229
-
230
- - "Can you find tools with higher accuracy?"
231
- - "Show me open-source alternatives"
232
- - "What about tools that support DICOM?"
233
-
234
- !!! tip "Ask Follow-Up Questions"
235
- The agent maintains context:
236
-
237
- - "What about the second recommendation?"
238
- - "Can you compare these two tools?"
239
- - "Which one is fastest?"
240
-
241
- ## Troubleshooting
242
-
243
- ### No Recommendations
244
-
245
- If the agent can't find suitable tools:
246
-
247
- - Try rephrasing your query
248
- - Be more specific about the task
249
- - Check that your file uploaded successfully
250
- - Ensure your task matches the catalog domain (imaging/medical)
251
-
252
- ### Wrong Recommendations
253
-
254
- If recommendations don't match:
255
-
256
- - Provide more context about your specific needs
257
- - Mention required file format support
258
- - Specify modality or domain
259
- - Use the exclude feature to filter out irrelevant tools
260
-
261
- ### Demo Execution Fails
262
-
263
- If a demo doesn't run:
264
-
265
- - Check your internet connection
266
- - Verify the demo link is still active
267
- - Try a different recommended tool
268
- - Check file format compatibility
269
-
270
- ## Next Steps
271
-
272
- - Learn about [Supported File Formats](file-formats.md)
273
- - Understand [How Recommendations Work](recommendations.md)
274
- - Explore [Running Demos](running-demos.md)
275
- - Check out [Advanced Features](advanced-features.md)
 
1
+ # Using the Chat Interface
2
+
3
+ The AI Imaging Agent provides a conversational interface for discovering and using imaging software. This guide explains how to interact with the chat interface effectively.
4
+
5
+ ## Interface Layout
6
+
7
+ The interface consists of three main areas:
8
+
9
+ ### Left Panel: Chat Conversation
10
+ - **Message History**: Your conversation with the agent
11
+ - **Rich Media Rendering**: Images, tool cards, and files are displayed inline
12
+ - **Input Box**: Type your messages at the bottom
13
+ - **File Upload**: Attach files via the paperclip icon or drag-and-drop
14
+
15
+ ### Right Panel: Sidebar
16
+ - **Files Tab**: View uploaded files with format information
17
+ - **State Tab**: Debug information showing conversation state
18
+
19
+ ### Header
20
+ - **Model Selector**: Choose which AI model to use
21
+ - **Settings**: Access configuration options
22
+
23
+ ## Basic Workflow
24
+
25
+ ### 1. Upload Files
26
+
27
+ Upload images or other files in several ways:
28
+
29
+ - **Drag and Drop**: Drag files directly onto the upload area
30
+ - **Click to Browse**: Click the upload area to select files
31
+ - **Attach to Message**: Use the paperclip icon in the input box
32
+
33
+ Files are automatically processed and metadata is extracted.
34
+
35
+ ### 2. Describe Your Task
36
+
37
+ Use natural language to describe what you want to do:
38
+
39
+ !!! example "Good Task Descriptions"
40
+ - "I want to segment the lungs from this CT scan"
41
+ - "Help me detect tumors in this MRI"
42
+ - "I need to register these two brain images"
43
+ - "Extract text from this medical report"
44
+ - "Classify the organ shown in this ultrasound"
45
+
46
+ ### 3. Review Recommendations
47
+
48
+ The agent returns ranked tool recommendations with:
49
+
50
+ - **Tool Cards**: Each tool is presented in a card format
51
+ - **Accuracy Scores**: Confidence levels for each recommendation
52
+ - **Explanations**: Why each tool matches your request
53
+ - **Metadata**: Technical details about compatibility
54
+
55
+ ### 4. Run Demos (Optional)
56
+
57
+ The agent may offer to run demos:
58
+
59
+ ```
60
+ Agent: Would you like me to run the demo with your image?
61
+ ```
62
+
63
+ Respond with affirmative language:
64
+ - "yes"
65
+ - "sure"
66
+ - "ok"
67
+ - "please"
68
+ - "go ahead"
69
+
70
+ The agent will execute the tool and show results.
71
+
72
+ ## Multi-Turn Conversations
73
+
74
+ The agent maintains context across multiple messages:
75
+
76
+ !!! example "Multi-Turn Example"
77
+ ```
78
+ You: I have a lung CT scan [uploads file]
79
+
80
+ Agent: I can see you have a DICOM CT image. What would you like to do with it?
81
+
82
+ You: Segment the airways
83
+
84
+ Agent: [Provides airway segmentation tool recommendations]
85
+
86
+ You: What about segmenting the whole lung?
87
+
88
+ Agent: [Provides lung segmentation tools, remembering you're working with CT]
89
+
90
+ You: Show me alternatives
91
+
92
+ Agent: [Provides additional options]
93
+ ```
94
+
95
+ ## Advanced Features
96
+
97
+ ### Excluding Tools
98
+
99
+ Exclude specific tools using the `[EXCLUDE:...]` tag:
100
+
101
+ ```
102
+ Find segmentation tools [EXCLUDE:totalsegmentator|medicalsam]
103
+ ```
104
+
105
+ You can exclude multiple tools separated by `|`.
106
+
107
+ ### Requesting Alternatives
108
+
109
+ Ask the agent to search with different strategies:
110
+
111
+ ```
112
+ Can you search for alternatives?
113
+
114
+ Show me other options
115
+
116
+ Find different tools for this task
117
+ ```
118
+
119
+ The agent can perform up to 3 alternative searches per conversation.
120
+
121
+ ## Understanding Agent Responses
122
+
123
+ ### Recommendation Cards
124
+
125
+ Each recommendation includes:
126
+
127
+ #### Header
128
+ - **Rank Number**: 1, 2, 3 (1 = best match)
129
+ - **Tool Name**: Software/tool identifier
130
+ - **Accuracy Score**: 0-100% confidence
131
+
132
+ #### Body
133
+ - **Description**: What the tool does
134
+ - **Explanation**: Why it matches your task
135
+ - **Demo Link**: Click to visit runnable example
136
+
137
+ #### Footer Metadata
138
+ - **Modalities**: CT, MRI, X-ray, etc.
139
+ - **Dimensions**: 2D, 3D, 4D
140
+ - **Formats**: Supported file formats (DICOM, NIfTI, etc.)
141
+ - **License**: Software license information
142
+ - **Tags**: Categorization and keywords
143
+
144
+ ### Execution Traces
145
+
146
+ When demos run, you'll see execution details:
147
+
148
+ ```
149
+ <details>
150
+ <summary>Tool Execution Trace</summary>
151
+
152
+ Image uploaded to Gradio Space
153
+ Processing started...
154
+ Result: Success
155
+ Output saved to: result.png
156
+ </details>
157
+ ```
158
+
159
+ Click to expand and see full execution logs.
160
+
161
+ ### Clarification Questions
162
+
163
+ Sometimes the agent needs more information:
164
+
165
+ ```
166
+ Agent: I found several segmentation tools. Which organ are you trying to segment?
167
+
168
+ You: The liver
169
+
170
+ Agent: [Provides liver-specific segmentation tools]
171
+ ```
172
+
173
+ ## File Management
174
+
175
+ ### Uploaded Files List
176
+
177
+ The sidebar shows all uploaded files with:
178
+
179
+ - **Filename**: Original file name
180
+ - **Format**: File type/extension
181
+ - **Size**: File size
182
+ - **Preview**: Thumbnail (for images)
183
+
184
+ ### Image Previews
185
+
186
+ Medical images are automatically converted:
187
+
188
+ - **DICOM**: PNG previews; 3D series use orthogonal composite views (MIPs + central slices) rather than a single slice
189
+ - **NIfTI**: PNG previews built from orthogonal composite views of the volume
190
+ - **TIFF Stacks**: PNG previews built from orthogonal composite views of the stack
191
+ - **Standard 2D Images**: Resized PNG preview of the original image
192
+
193
+ Previews are used for VLM analysis while preserving original format metadata.
194
+
195
+ ### Removing Files
196
+
197
+ Click the 'X' button next to a file to remove it from the current session.
198
+
199
+ ## Conversation State
200
+
201
+ The debug sidebar shows:
202
+
203
+ ### Current State
204
+ - **Status**: idle, processing, waiting
205
+ - **Conversation Turn**: Current turn number
206
+ - **Excluded Tools**: Tools filtered from results
207
+
208
+ ### Preview Images
209
+ - Images prepared for VLM analysis
210
+ - Format conversions applied
211
+
212
+ ## Tips for Effective Interaction
213
+
214
+ !!! tip "Be Specific About Requirements"
215
+ Mention specific needs:
216
+
217
+ - "I need a tool that works with NIfTI files"
218
+ - "Must support 3D volumes"
219
+ - "Looking for open-source options"
220
+
221
+ !!! tip "Use Conversational Language"
222
+ Natural language works best:
223
+
224
+ - ✅ "Help me find tool that segments kidneys"
225
+ - ❌ "kidney_segmentation_tool filter:3D"
226
+
227
+ !!! tip "Iterate Based on Results"
228
+ If initial results aren't perfect, refine:
229
+
230
+ - "Can you find tools with higher accuracy?"
231
+ - "Show me open-source alternatives"
232
+ - "What about tools that support DICOM?"
233
+
234
+ !!! tip "Ask Follow-Up Questions"
235
+ The agent maintains context:
236
+
237
+ - "What about the second recommendation?"
238
+ - "Can you compare these two tools?"
239
+ - "Which one is fastest?"
240
+
241
+ ## Troubleshooting
242
+
243
+ ### No Recommendations
244
+
245
+ If the agent can't find suitable tools:
246
+
247
+ - Try rephrasing your query
248
+ - Be more specific about the task
249
+ - Check that your file uploaded successfully
250
+ - Ensure your task matches the catalog domain (imaging/medical)
251
+
252
+ ### Wrong Recommendations
253
+
254
+ If recommendations don't match:
255
+
256
+ - Provide more context about your specific needs
257
+ - Mention required file format support
258
+ - Specify modality or domain
259
+ - Use the exclude feature to filter out irrelevant tools
260
+
261
+ ### Demo Execution Fails
262
+
263
+ If a demo doesn't run:
264
+
265
+ - Check your internet connection
266
+ - Verify the demo link is still active
267
+ - Try a different recommended tool
268
+ - Check file format compatibility
269
+
270
+ ## Next Steps
271
+
272
+ - Learn about [Supported File Formats](file-formats.md)
273
+ - Understand [How Recommendations Work](recommendations.md)
274
+ - Explore [Running Demos](running-demos.md)
275
+ - Check out [Advanced Features](advanced-features.md)
docs/user-guide/file-formats.md CHANGED
@@ -1,282 +1,282 @@
1
- # Supported File Formats
2
-
3
- The AI Imaging Agent supports a wide range of file formats for medical and scientific imaging, as well as general data files.
4
-
5
- ## Image Formats
6
-
7
- ### Standard Images
8
-
9
- | Format | Extensions | Description |
10
- |--------|-----------|-------------|
11
- | PNG | `.png` | Portable Network Graphics - lossless compression |
12
- | JPEG | `.jpg`, `.jpeg` | Joint Photographic Experts Group - lossy compression |
13
-
14
- Currently, only PNG and JPEG are accepted for standard images. Other web formats (e.g. WebP, BMP, GIF) should be converted to PNG or JPEG before upload.
15
- **Best for**: General photographs, screenshots, web images
16
-
17
- ### Medical Imaging Formats
18
-
19
- #### DICOM
20
-
21
- | Format | Extensions | Description |
22
- |--------|-----------|-------------|
23
- | DICOM | `.dcm`, `.dicom` | Digital Imaging and Communications in Medicine |
24
-
25
- **Features**:
26
-
27
- - Industry standard for medical imaging
28
- - Contains rich metadata (patient info, acquisition parameters)
29
- - Supports multiple modalities (CT, MRI, X-ray, etc.)
30
- - Can store 2D images or 3D volumes
31
-
32
- **Metadata Extracted**:
33
-
34
- - Patient ID, Study Instance UID
35
- - Modality (CT, MR, CR, DX, etc.)
36
- - Image dimensions and spacing
37
- - Acquisition date/time
38
- - Manufacturer and model
39
-
40
- **Example Usage**:
41
- ```
42
- Upload a CT DICOM file and ask:
43
- "Segment the lungs from this scan"
44
- ```
45
-
46
- #### NIfTI
47
-
48
- | Format | Extensions | Description |
49
- |--------|-----------|-------------|
50
- | NIfTI | `.nii`, `.nii.gz` | Neuroimaging Informatics Technology Initiative |
51
-
52
- **Features**:
53
-
54
- - Standard for neuroimaging research
55
- - Supports 3D and 4D (time-series) volumes
56
- - Compact storage with optional gzip compression
57
- - Contains spatial orientation information
58
-
59
- **Metadata Extracted**:
60
-
61
- - Volume dimensions (x, y, z, time)
62
- - Voxel spacing
63
- - Data type and bit depth
64
- - Orientation matrix
65
-
66
- **Example Usage**:
67
- ```
68
- Upload a brain MRI NIfTI file:
69
- "Register this brain scan to MNI space"
70
- ```
71
-
72
- ### Scientific Imaging Formats
73
-
74
- #### TIFF/TIFF Stacks
75
-
76
- | Format | Extensions | Description |
77
- |--------|-----------|-------------|
78
- | TIFF | `.tif`, `.tiff` | Tagged Image File Format |
79
-
80
- **Features**:
81
-
82
- - Supports multi-page/multi-frame images
83
- - Common in microscopy and scientific imaging
84
- - Can store extensive metadata
85
- - Lossless compression options
86
-
87
- **Metadata Extracted**:
88
-
89
- - Number of pages/frames (for stacks)
90
- - Dimensions (width, height, channels)
91
- - Color mode (RGB, grayscale, etc.)
92
- - Compression method
93
- - DPI/resolution information
94
-
95
- **Example Usage**:
96
- ```
97
- Upload a microscopy TIFF stack:
98
- "Analyze cell structures in this z-stack"
99
- ```
100
-
101
- ## Data Formats
102
-
103
- <!-- ### Structured Data
104
-
105
- | Format | Extensions | Description |
106
- |--------|-----------|-------------|
107
- | CSV | `.csv` | Comma-separated values |
108
- | JSON | `.json` | JavaScript Object Notation |
109
- | XML | `.xml` | Extensible Markup Language |
110
-
111
- **Best for**: Metadata, annotations, measurements, structured results
112
-
113
- ## Media Formats
114
-
115
- | Format | Extensions | Description |
116
- |--------|-----------|-------------|
117
- | Audio | `.mp3` | MPEG Audio Layer 3 |
118
- | Video | `.mp4` | MPEG-4 video |
119
-
120
- **Note**: Currently supported for upload but limited analysis capabilities. -->
121
-
122
- ## Format Detection
123
-
124
- The agent automatically detects file formats using:
125
-
126
- 1. **File Extension**: Primary detection method
127
- 2. **Magic Bytes**: Header inspection for validation
128
- 3. **Content Analysis**: Fallback for ambiguous cases
129
-
130
- ## Metadata Extraction
131
-
132
- ### What Gets Extracted
133
-
134
- For each uploaded file, the agent extracts:
135
-
136
- #### Image Metadata
137
- - **Dimensions**: Width, height, depth (for volumes)
138
- - **Channels**: Grayscale, RGB, RGBA
139
- - **Data Type**: uint8, int16, float32, etc.
140
- - **File Size**: Storage size
141
-
142
- #### Medical Image Metadata
143
- - **Modality**: CT, MRI, X-ray, Ultrasound, PET, etc.
144
- - **Patient Info**: Anonymized IDs
145
- - **Study Info**: Study UID, dates
146
- - **Acquisition Parameters**: Slice thickness, spacing, orientation
147
- - **Equipment**: Manufacturer, model, software version
148
-
149
- #### Format-Specific Metadata
150
- - **DICOM Tags**: Full DICOM header information
151
- - **NIfTI Header**: Spatial orientation, timing information
152
- - **TIFF Tags**: IFD entries, compression, photometric interpretation
153
-
154
- ### Why Metadata Matters
155
-
156
- Metadata is used for:
157
-
158
- 1. **Format Matching**: Recommend tools that support your file format
159
- 2. **Compatibility Scoring**: Prioritize tools that work with your specific format
160
- 3. **Context Understanding**: Help VLM understand image characteristics
161
- 4. **Demo Execution**: Ensure tools can process your data
162
-
163
- ## Preview Generation
164
-
165
- ### Automatic Conversion
166
-
167
- Medical and scientific images are converted to PNG previews for VLM analysis:
168
-
169
- | Original Format | Preview Generation |
170
- |----------------|-------------------|
171
- | DICOM (2D) | Single-frame converted to PNG |
172
- | DICOM / NIfTI 3D volumes | Orthogonal 3‑view composite PNG (axial, sagittal, coronal) using middle slices and/or maximum intensity projections (MIPs) |
173
- | NIfTI 4D (time series) | Middle timepoint volume rendered as an orthogonal 3‑view composite (middle slices and/or MIPs) |
174
- | TIFF Stack | Orthogonal 3‑view composite for 3D stacks; otherwise contact sheet or animated GIF preview when appropriate |
175
- | Standard Images | Single-view PNG (content preserved; may be resized/normalized) |
176
-
177
- **Important**: Preview generation is for visual analysis only. Original format metadata is preserved and used for compatibility matching.
178
-
179
- ### Multi-Slice Handling
180
-
181
- For 3D volumes, the agent typically builds an orthogonal 3‑view composite preview:
182
-
183
- - **Axial**: Horizontal slices (z-axis)
184
- - **Sagittal**: Side view (x-axis)
185
- - **Coronal**: Front view (y-axis)
186
-
187
- Each view may combine the middle slice with a maximum intensity projection (MIP) to capture both anatomical context and bright structures. When a 3‑view composite cannot be generated (e.g., unusual stack layout), the agent may fall back to a contact sheet or an animated GIF preview of multiple slices.
188
-
189
- ## Format Compatibility Matching
190
-
191
- ### How It Works
192
-
193
- The retrieval system adds format tokens to your query:
194
-
195
- ```
196
- Original query: "segment lungs"
197
- Enhanced query: "segment lungs format:DICOM format:3D"
198
- ```
199
-
200
- Tools are matched based on:
201
-
202
- 1. **Direct Format Support**: Tool explicitly supports your format
203
- 2. **Format Category**: Tool supports format family (e.g., medical imaging)
204
- 3. **Conversion Capability**: Tool can convert from your format
205
-
206
- ### IO Compatibility Scoring
207
-
208
- The VLM considers:
209
-
210
- - **Input Format Match**: Can the tool read your file?
211
- - **Output Format**: What format does the tool produce?
212
- - **Dimension Compatibility**: 2D tool for 2D images, 3D for volumes
213
- - **Modality Specificity**: CT tools for CT images, MRI for MRI
214
-
215
- <!-- ## File Size Limits
216
-
217
- Default limits (configurable):
218
-
219
- | Category | Limit | Notes |
220
- |----------|-------|-------|
221
- | Images | 100 MB | Per file |
222
- | DICOM | 200 MB | Medical images can be larger |
223
- | NIfTI | 500 MB | Volumes can be very large |
224
- | TIFF Stacks | 200 MB | Multi-frame images |
225
- | Other Files | 50 MB | General limit |
226
-
227
- !!! warning "Large Files"
228
- Very large files may take longer to process. Consider downsampling or cropping if possible. -->
229
-
230
- ## Unsupported Formats
231
-
232
- Currently not supported:
233
-
234
- - **Proprietary Formats**: Manufacturer-specific formats (e.g., .PAR/.REC)
235
- - **Video Processing**: Limited video analysis capability
236
- - **Raw Data**: Unformatted binary dumps without headers
237
-
238
- ## Format Best Practices
239
-
240
- !!! tip "Use Standard Formats"
241
- Stick to standard formats (DICOM, NIfTI, PNG, TIFF) for best tool compatibility.
242
-
243
- !!! tip "Include Metadata"
244
- Use formats that preserve metadata (DICOM, NIfTI) rather than exporting to PNG/JPEG.
245
-
246
- !!! tip "Check Compatibility"
247
- If a tool doesn't work, check the format compatibility in the recommendation metadata.
248
-
249
- !!! tip "Convert When Needed"
250
- Some tools prefer specific formats. Convert using standard tools (ITK-SNAP, 3D Slicer) before upload.
251
-
252
- ## Example Workflows by Format
253
-
254
- ### DICOM Workflow
255
- ```
256
- 1. Upload: chest_ct.dcm
257
- 2. Query: "Segment lungs"
258
- 3. Agent detects: DICOM, CT modality, 3D volume
259
- 4. Results: CT-compatible lung segmentation tools
260
- ```
261
-
262
- ### NIfTI Workflow
263
- ```
264
- 1. Upload: brain_mri.nii.gz
265
- 2. Query: "Skull stripping"
266
- 3. Agent detects: NIfTI, 3D volume, likely MRI
267
- 4. Results: Brain extraction tools supporting NIfTI
268
- ```
269
-
270
- ### TIFF Stack Workflow
271
- ```
272
- 1. Upload: microscopy_stack.tif
273
- 2. Query: "Cell counting"
274
- 3. Agent detects: Multi-frame TIFF, 3D stack
275
- 4. Results: Microscopy analysis tools
276
- ```
277
-
278
- ## Next Steps
279
-
280
- - Learn about [Understanding Recommendations](recommendations.md)
281
- - Explore [Running Demos](running-demos.md)
282
- - Check [Advanced Features](advanced-features.md)
 
1
+ # Supported File Formats
2
+
3
+ The AI Imaging Agent supports a wide range of file formats for medical and scientific imaging, as well as general data files.
4
+
5
+ ## Image Formats
6
+
7
+ ### Standard Images
8
+
9
+ | Format | Extensions | Description |
10
+ |--------|-----------|-------------|
11
+ | PNG | `.png` | Portable Network Graphics - lossless compression |
12
+ | JPEG | `.jpg`, `.jpeg` | Joint Photographic Experts Group - lossy compression |
13
+
14
+ Currently, only PNG and JPEG are accepted for standard images. Other web formats (e.g. WebP, BMP, GIF) should be converted to PNG or JPEG before upload.
15
+ **Best for**: General photographs, screenshots, web images
16
+
17
+ ### Medical Imaging Formats
18
+
19
+ #### DICOM
20
+
21
+ | Format | Extensions | Description |
22
+ |--------|-----------|-------------|
23
+ | DICOM | `.dcm`, `.dicom` | Digital Imaging and Communications in Medicine |
24
+
25
+ **Features**:
26
+
27
+ - Industry standard for medical imaging
28
+ - Contains rich metadata (patient info, acquisition parameters)
29
+ - Supports multiple modalities (CT, MRI, X-ray, etc.)
30
+ - Can store 2D images or 3D volumes
31
+
32
+ **Metadata Extracted**:
33
+
34
+ - Patient ID, Study Instance UID
35
+ - Modality (CT, MR, CR, DX, etc.)
36
+ - Image dimensions and spacing
37
+ - Acquisition date/time
38
+ - Manufacturer and model
39
+
40
+ **Example Usage**:
41
+ ```
42
+ Upload a CT DICOM file and ask:
43
+ "Segment the lungs from this scan"
44
+ ```
45
+
46
+ #### NIfTI
47
+
48
+ | Format | Extensions | Description |
49
+ |--------|-----------|-------------|
50
+ | NIfTI | `.nii`, `.nii.gz` | Neuroimaging Informatics Technology Initiative |
51
+
52
+ **Features**:
53
+
54
+ - Standard for neuroimaging research
55
+ - Supports 3D and 4D (time-series) volumes
56
+ - Compact storage with optional gzip compression
57
+ - Contains spatial orientation information
58
+
59
+ **Metadata Extracted**:
60
+
61
+ - Volume dimensions (x, y, z, time)
62
+ - Voxel spacing
63
+ - Data type and bit depth
64
+ - Orientation matrix
65
+
66
+ **Example Usage**:
67
+ ```
68
+ Upload a brain MRI NIfTI file:
69
+ "Register this brain scan to MNI space"
70
+ ```
71
+
72
+ ### Scientific Imaging Formats
73
+
74
+ #### TIFF/TIFF Stacks
75
+
76
+ | Format | Extensions | Description |
77
+ |--------|-----------|-------------|
78
+ | TIFF | `.tif`, `.tiff` | Tagged Image File Format |
79
+
80
+ **Features**:
81
+
82
+ - Supports multi-page/multi-frame images
83
+ - Common in microscopy and scientific imaging
84
+ - Can store extensive metadata
85
+ - Lossless compression options
86
+
87
+ **Metadata Extracted**:
88
+
89
+ - Number of pages/frames (for stacks)
90
+ - Dimensions (width, height, channels)
91
+ - Color mode (RGB, grayscale, etc.)
92
+ - Compression method
93
+ - DPI/resolution information
94
+
95
+ **Example Usage**:
96
+ ```
97
+ Upload a microscopy TIFF stack:
98
+ "Analyze cell structures in this z-stack"
99
+ ```
100
+
101
+ ## Data Formats
102
+
103
+ <!-- ### Structured Data
104
+
105
+ | Format | Extensions | Description |
106
+ |--------|-----------|-------------|
107
+ | CSV | `.csv` | Comma-separated values |
108
+ | JSON | `.json` | JavaScript Object Notation |
109
+ | XML | `.xml` | Extensible Markup Language |
110
+
111
+ **Best for**: Metadata, annotations, measurements, structured results
112
+
113
+ ## Media Formats
114
+
115
+ | Format | Extensions | Description |
116
+ |--------|-----------|-------------|
117
+ | Audio | `.mp3` | MPEG Audio Layer 3 |
118
+ | Video | `.mp4` | MPEG-4 video |
119
+
120
+ **Note**: Currently supported for upload but limited analysis capabilities. -->
121
+
122
+ ## Format Detection
123
+
124
+ The agent automatically detects file formats using:
125
+
126
+ 1. **File Extension**: Primary detection method
127
+ 2. **Magic Bytes**: Header inspection for validation
128
+ 3. **Content Analysis**: Fallback for ambiguous cases
129
+
130
+ ## Metadata Extraction
131
+
132
+ ### What Gets Extracted
133
+
134
+ For each uploaded file, the agent extracts:
135
+
136
+ #### Image Metadata
137
+ - **Dimensions**: Width, height, depth (for volumes)
138
+ - **Channels**: Grayscale, RGB, RGBA
139
+ - **Data Type**: uint8, int16, float32, etc.
140
+ - **File Size**: Storage size
141
+
142
+ #### Medical Image Metadata
143
+ - **Modality**: CT, MRI, X-ray, Ultrasound, PET, etc.
144
+ - **Patient Info**: Anonymized IDs
145
+ - **Study Info**: Study UID, dates
146
+ - **Acquisition Parameters**: Slice thickness, spacing, orientation
147
+ - **Equipment**: Manufacturer, model, software version
148
+
149
+ #### Format-Specific Metadata
150
+ - **DICOM Tags**: Full DICOM header information
151
+ - **NIfTI Header**: Spatial orientation, timing information
152
+ - **TIFF Tags**: IFD entries, compression, photometric interpretation
153
+
154
+ ### Why Metadata Matters
155
+
156
+ Metadata is used for:
157
+
158
+ 1. **Format Matching**: Recommend tools that support your file format
159
+ 2. **Compatibility Scoring**: Prioritize tools that work with your specific format
160
+ 3. **Context Understanding**: Help VLM understand image characteristics
161
+ 4. **Demo Execution**: Ensure tools can process your data
162
+
163
+ ## Preview Generation
164
+
165
+ ### Automatic Conversion
166
+
167
+ Medical and scientific images are converted to PNG previews for VLM analysis:
168
+
169
+ | Original Format | Preview Generation |
170
+ |----------------|-------------------|
171
+ | DICOM (2D) | Single-frame converted to PNG |
172
+ | DICOM / NIfTI 3D volumes | Orthogonal 3‑view composite PNG (axial, sagittal, coronal) using middle slices and/or maximum intensity projections (MIPs) |
173
+ | NIfTI 4D (time series) | Middle timepoint volume rendered as an orthogonal 3‑view composite (middle slices and/or MIPs) |
174
+ | TIFF Stack | Orthogonal 3‑view composite for 3D stacks; otherwise contact sheet or animated GIF preview when appropriate |
175
+ | Standard Images | Single-view PNG (content preserved; may be resized/normalized) |
176
+
177
+ **Important**: Preview generation is for visual analysis only. Original format metadata is preserved and used for compatibility matching.
178
+
179
+ ### Multi-Slice Handling
180
+
181
+ For 3D volumes, the agent typically builds an orthogonal 3‑view composite preview:
182
+
183
+ - **Axial**: Horizontal slices (z-axis)
184
+ - **Sagittal**: Side view (x-axis)
185
+ - **Coronal**: Front view (y-axis)
186
+
187
+ Each view may combine the middle slice with a maximum intensity projection (MIP) to capture both anatomical context and bright structures. When a 3‑view composite cannot be generated (e.g., unusual stack layout), the agent may fall back to a contact sheet or an animated GIF preview of multiple slices.
188
+
189
+ ## Format Compatibility Matching
190
+
191
+ ### How It Works
192
+
193
+ The retrieval system adds format tokens to your query:
194
+
195
+ ```
196
+ Original query: "segment lungs"
197
+ Enhanced query: "segment lungs format:DICOM format:3D"
198
+ ```
199
+
200
+ Tools are matched based on:
201
+
202
+ 1. **Direct Format Support**: Tool explicitly supports your format
203
+ 2. **Format Category**: Tool supports format family (e.g., medical imaging)
204
+ 3. **Conversion Capability**: Tool can convert from your format
205
+
206
+ ### IO Compatibility Scoring
207
+
208
+ The VLM considers:
209
+
210
+ - **Input Format Match**: Can the tool read your file?
211
+ - **Output Format**: What format does the tool produce?
212
+ - **Dimension Compatibility**: 2D tool for 2D images, 3D for volumes
213
+ - **Modality Specificity**: CT tools for CT images, MRI for MRI
214
+
215
+ <!-- ## File Size Limits
216
+
217
+ Default limits (configurable):
218
+
219
+ | Category | Limit | Notes |
220
+ |----------|-------|-------|
221
+ | Images | 100 MB | Per file |
222
+ | DICOM | 200 MB | Medical images can be larger |
223
+ | NIfTI | 500 MB | Volumes can be very large |
224
+ | TIFF Stacks | 200 MB | Multi-frame images |
225
+ | Other Files | 50 MB | General limit |
226
+
227
+ !!! warning "Large Files"
228
+ Very large files may take longer to process. Consider downsampling or cropping if possible. -->
229
+
230
+ ## Unsupported Formats
231
+
232
+ Currently not supported:
233
+
234
+ - **Proprietary Formats**: Manufacturer-specific formats (e.g., .PAR/.REC)
235
+ - **Video Processing**: Limited video analysis capability
236
+ - **Raw Data**: Unformatted binary dumps without headers
237
+
238
+ ## Format Best Practices
239
+
240
+ !!! tip "Use Standard Formats"
241
+ Stick to standard formats (DICOM, NIfTI, PNG, TIFF) for best tool compatibility.
242
+
243
+ !!! tip "Include Metadata"
244
+ Use formats that preserve metadata (DICOM, NIfTI) rather than exporting to PNG/JPEG.
245
+
246
+ !!! tip "Check Compatibility"
247
+ If a tool doesn't work, check the format compatibility in the recommendation metadata.
248
+
249
+ !!! tip "Convert When Needed"
250
+ Some tools prefer specific formats. Convert using standard tools (ITK-SNAP, 3D Slicer) before upload.
251
+
252
+ ## Example Workflows by Format
253
+
254
+ ### DICOM Workflow
255
+ ```
256
+ 1. Upload: chest_ct.dcm
257
+ 2. Query: "Segment lungs"
258
+ 3. Agent detects: DICOM, CT modality, 3D volume
259
+ 4. Results: CT-compatible lung segmentation tools
260
+ ```
261
+
262
+ ### NIfTI Workflow
263
+ ```
264
+ 1. Upload: brain_mri.nii.gz
265
+ 2. Query: "Skull stripping"
266
+ 3. Agent detects: NIfTI, 3D volume, likely MRI
267
+ 4. Results: Brain extraction tools supporting NIfTI
268
+ ```
269
+
270
+ ### TIFF Stack Workflow
271
+ ```
272
+ 1. Upload: microscopy_stack.tif
273
+ 2. Query: "Cell counting"
274
+ 3. Agent detects: Multi-frame TIFF, 3D stack
275
+ 4. Results: Microscopy analysis tools
276
+ ```
277
+
278
+ ## Next Steps
279
+
280
+ - Learn about [Understanding Recommendations](recommendations.md)
281
+ - Explore [Running Demos](running-demos.md)
282
+ - Check [Advanced Features](advanced-features.md)
docs/user-guide/recommendations.md CHANGED
@@ -1,329 +1,329 @@
1
- # Understanding Recommendations
2
-
3
- The AI Imaging Agent uses a sophisticated two-stage pipeline to provide ranked tool recommendations. This guide explains how recommendations are generated and how to interpret them.
4
-
5
- ## How Recommendations Work
6
-
7
- ### Two-Stage Pipeline
8
-
9
- ```mermaid
10
- graph TD
11
- A[User Input: Image + Query] --> B[Stage 1: Retrieval]
12
- B --> C[Candidate Tools]
13
- C --> D[Stage 2: Agent Selection]
14
- D --> E[Ranked Recommendations]
15
- ```
16
-
17
- #### Stage 1: Retrieval (Fast Text Search)
18
-
19
- The retrieval stage quickly narrows down candidates:
20
-
21
- 1. **Query Enhancement**: Your query is enriched with format tokens
22
- ```
23
- Original: "segment lungs"
24
- Enhanced: "segment lungs format:DICOM format:CT format:3D"
25
- ```
26
-
27
- 2. **Embedding Search**: BGE-M3 model converts query to vector
28
- 3. **FAISS Vector Search**: Finds semantically similar tools
29
- 4. **CrossEncoder Reranking**: Re-scores candidates for better relevance
30
- 5. **Result**: Top-K candidates (default: 8)
31
-
32
- **No LLM calls** - this stage is fast and deterministic.
33
-
34
- #### Stage 2: Agent Selection (VLM-Powered)
35
-
36
- The agent analyzes candidates with full context:
37
-
38
- 1. **Vision Analysis (only for VLM)**: GPT-4o/4o-mini (or your custom model) sees your image preview
39
- 2. **Context Integration**: Considers query + metadata + candidates
40
- 3. **Reasoning**: Explains why each tool matches
41
- 4. **Scoring**: Assigns accuracy scores (0-100%)
42
- 5. **Ranking**: Orders tools by relevance
43
-
44
- **Single VLM call** - comprehensive analysis with explanations.
45
-
46
- ## Recommendation Format
47
-
48
- Each recommendation includes several components:
49
-
50
- ### Header Information
51
-
52
- #### Rank Number
53
- Position in the ranked list (1 = best match).
54
-
55
- ```
56
- 1️⃣ TotalSegmentator
57
- 2️⃣ MedSAM
58
- 3️⃣ nnU-Net
59
- ```
60
-
61
- #### Tool Name
62
- The software or tool identifier, typically matching:
63
- - GitHub repository name
64
- - Published tool name
65
- - Common community name
66
-
67
- #### Accuracy Score
68
- Confidence level from 0-100%:
69
-
70
- - **90-100%**: Excellent match, highly confident
71
- - **70-89%**: Good match, suitable for task
72
- - **50-69%**: Moderate match, may need adaptation
73
- - **Below 50%**: Weak match, alternative approach
74
-
75
- !!! note "Score Interpretation"
76
- Scores reflect match quality for **your specific task and image**, not overall tool quality.
77
-
78
- ### Body Content
79
-
80
- #### Description
81
- Brief explanation of what the tool does:
82
-
83
- ```
84
- TotalSegmentator: Automated multi-organ segmentation for CT scans supporting 104 anatomical structures.
85
- ```
86
-
87
- #### Explanation
88
- Why this tool matches your request:
89
-
90
- ```
91
- Explanation: TotalSegmentator is specifically designed for whole-body CT segmentation including lung structures. It supports DICOM input and provides automated, accurate lung segmentation without manual intervention.
92
- ```
93
-
94
- Key points in explanations:
95
-
96
- - **Task Alignment**: How well it matches your goal
97
- - **Format Compatibility**: Support for your file format
98
- - **Relevant Features**: Specific capabilities that help
99
- - **Known Limitations**: Caveats or requirements
100
-
101
- #### Demo Link
102
- Direct link to a runnable example:
103
-
104
- ```
105
- 🚀 Demo: https://huggingface.co/spaces/example/totalsegmentator
106
- ```
107
-
108
- Types of demos:
109
-
110
- - **HuggingFace Spaces**: Interactive Gradio/Streamlit apps
111
- - **Colab Notebooks**: Jupyter notebooks you can run
112
- - **Web Demos**: Hosted web interfaces
113
- - **Documentation**: GitHub README with examples
114
-
115
- ### Metadata Footer
116
-
117
- Technical details about the tool:
118
-
119
- #### Modality Support
120
- Medical imaging modalities the tool works with:
121
-
122
- ```
123
- Modalities: CT, MRI, X-ray
124
- ```
125
-
126
- Common modalities:
127
-
128
- - **CT**: Computed Tomography
129
- - **MRI**: Magnetic Resonance Imaging
130
- - **XR**: X-ray radiography
131
- - **US**: Ultrasound
132
- - **PET**: Positron Emission Tomography
133
- - **OCT**: Optical Coherence Tomography
134
- - **Microscopy**: Various microscopy types
135
-
136
- #### Dimension Support
137
- Image/volume dimensions supported:
138
-
139
- ```
140
- Dimensions: 2D, 3D
141
- ```
142
-
143
- - **2D**: Single slice images
144
- - **3D**: Volumetric data
145
- - **4D**: Time-series volumes
146
-
147
- #### Format Support
148
- File formats the tool can process:
149
-
150
- ```
151
- Formats: DICOM, NIfTI, PNG, JPEG
152
- ```
153
-
154
- !!! tip "Format Importance"
155
- Tools that support your **exact format** are prioritized in ranking.
156
-
157
- #### Tags
158
- Categorization and keywords:
159
-
160
- ```
161
- Tags: segmentation, medical-imaging, deep-learning, pytorch
162
- ```
163
-
164
- Used for:
165
- - Task categorization
166
- - Technology stack
167
- - Domain specificity
168
- - Feature indicators
169
-
170
- ## Scoring Factors
171
-
172
- The agent considers multiple factors when scoring:
173
-
174
- ### Primary Factors (High Weight)
175
-
176
- 1. **Task Match**: How well the tool's purpose aligns with your request
177
- 2. **Format Compatibility**: Support for your input format
178
- 3. **Image Content**: Visual analysis of what's in your image
179
- 4. **Dimension Match**: 2D tool for 2D images, 3D for volumes
180
-
181
- ### Secondary Factors (Medium Weight)
182
-
183
- 5. **Modality Specificity**: Tool designed for your imaging modality
184
- 6. **Feature Coverage**: Breadth of capabilities
185
- 7. **Stated Requirements**: Meets any specific requirements you mentioned
186
- 8. **Quality Indicators**: Stars, citations, community adoption
187
-
188
- ### Tertiary Factors (Low Weight)
189
-
190
- 9. **License**: Open-source vs. proprietary
191
- 10. **Recency**: Recently updated tools
192
- 11. **Documentation Quality**: Demo availability, examples
193
- 12. **Popularity**: Community usage and validation
194
-
195
- ## Interpreting Results
196
-
197
- ### High-Scoring Recommendations
198
-
199
- When you see scores above 85%:
200
-
201
- ✅ **Strong match** - Tool is designed for this task
202
- ✅ **Format compatible** - Handles your file type
203
- ✅ **Proven capability** - Demonstrated results in this domain
204
-
205
- **Action**: These are your best options. Try the top recommendation first.
206
-
207
- ### Medium-Scoring Recommendations
208
-
209
- Scores 60-85%:
210
-
211
- ⚠️ **Good match** - Suitable but may need adaptation
212
- ⚠️ **Possible format conversion** - Might require preprocessing
213
- ⚠️ **Partial capability** - Covers some but not all requirements
214
-
215
- **Action**: Worth trying, especially if top choices don't work. Read explanations carefully.
216
-
217
- ### Low-Scoring Recommendations
218
-
219
- Scores below 60%:
220
-
221
- ❌ **Weak match** - Limited alignment with task
222
- ❌ **Format issues** - May not support your format
223
- ❌ **Alternative approach** - Different methodology
224
-
225
- **Action**: Consider as fallback or for exploring alternative approaches.
226
-
227
- ## Why Rankings Change
228
-
229
- Rankings depend on your specific context:
230
-
231
- ### Same Tool, Different Queries
232
-
233
- "Segment lungs" vs "Detect tumors":
234
- - Different tools excel at each task
235
- - Rankings change based on task specificity
236
-
237
- ### Same Task, Different Formats
238
-
239
- DICOM input vs PNG input:
240
- - DICOM-compatible tools rank higher for DICOM
241
- - General tools rank higher for standard images
242
-
243
- ### Same Task, Different Images
244
-
245
- CT scan vs X-ray:
246
- - Modality-specific tools get boosted
247
- - Visual content influences selection
248
-
249
- ## Common Patterns
250
-
251
- ### All High Scores
252
- Most recommendations >80%:
253
-
254
- - **Good news!** Multiple excellent options
255
- - **Strategy**: Try top recommendation, then compare
256
-
257
- ### Mixed Scores
258
- Wide range (e.g., 90%, 65%, 45%):
259
-
260
- - **Top choice clear** - Focus on highest scorer
261
- - **Strategy**: Try #1, fall back to #2 if needed
262
-
263
- ### All Low Scores
264
- All recommendations <60%:
265
-
266
- - **Limited options** - Task may be specialized
267
- - **Strategy**: Try anyway, or rephrase query
268
- - **Alternative**: Ask for suggestions
269
-
270
- ## Acting on Recommendations
271
-
272
- ### First Time with a Tool
273
-
274
- 1. **Read the explanation** - Understand why it was recommended
275
- 2. **Check format compatibility** - Verify it supports your format
276
- 3. **Click demo link** - See it in action
277
- 4. **Try on your data** - Run if agent offers
278
-
279
- ### Comparing Tools
280
-
281
- When choosing between similar scores:
282
-
283
- - **Check licenses** if redistribution matters
284
- - **Compare formats** - prefer exact format match
285
- - **Review tags** - match technology preferences
286
- - **Demo availability** - easier to try
287
-
288
- ### When Results Don't Match
289
-
290
- If recommendations seem wrong:
291
-
292
- 1. **Provide more context**: "I need 3D volume support"
293
- 2. **Mention specific requirements**: "Must work with DICOM"
294
- 3. **Exclude irrelevant tools**: `[EXCLUDE:toolname]`
295
- 4. **Request alternatives**: "Can you search differently?"
296
-
297
- ## Explanation Analysis
298
-
299
- Read explanations to understand:
300
-
301
- ### Positive Indicators
302
-
303
- Look for phrases like:
304
- - "Specifically designed for..."
305
- - "Supports your exact format..."
306
- - "Demonstrated accuracy on..."
307
- - "Active development and maintained"
308
-
309
- ### Caveats
310
-
311
- Watch for:
312
- - "May require preprocessing..."
313
- - "Limited to 2D images..."
314
- - "Experimental feature..."
315
- - "Requires specific environment..."
316
-
317
- ### Requirements
318
-
319
- Note when explanations mention:
320
- - "Needs GPU for inference"
321
- - "Requires Python 3.8+"
322
- - "DICOM headers must include..."
323
- - "Minimum image resolution..."
324
-
325
- ## Next Steps
326
-
327
- - Learn about [Running Demos](running-demos.md)
328
- - Explore [Advanced Features](advanced-features.md)
329
- - Understand the [Architecture Overview](../architecture/overview.md)
 
1
+ # Understanding Recommendations
2
+
3
+ The AI Imaging Agent uses a sophisticated two-stage pipeline to provide ranked tool recommendations. This guide explains how recommendations are generated and how to interpret them.
4
+
5
+ ## How Recommendations Work
6
+
7
+ ### Two-Stage Pipeline
8
+
9
+ ```mermaid
10
+ graph TD
11
+ A[User Input: Image + Query] --> B[Stage 1: Retrieval]
12
+ B --> C[Candidate Tools]
13
+ C --> D[Stage 2: Agent Selection]
14
+ D --> E[Ranked Recommendations]
15
+ ```
16
+
17
+ #### Stage 1: Retrieval (Fast Text Search)
18
+
19
+ The retrieval stage quickly narrows down candidates:
20
+
21
+ 1. **Query Enhancement**: Your query is enriched with format tokens
22
+ ```
23
+ Original: "segment lungs"
24
+ Enhanced: "segment lungs format:DICOM format:CT format:3D"
25
+ ```
26
+
27
+ 2. **Embedding Search**: BGE-M3 model converts query to vector
28
+ 3. **FAISS Vector Search**: Finds semantically similar tools
29
+ 4. **CrossEncoder Reranking**: Re-scores candidates for better relevance
30
+ 5. **Result**: Top-K candidates (default: 8)
31
+
32
+ **No LLM calls** - this stage is fast and deterministic.
33
+
34
+ #### Stage 2: Agent Selection (VLM-Powered)
35
+
36
+ The agent analyzes candidates with full context:
37
+
38
+ 1. **Vision Analysis (only for VLM)**: GPT-4o/4o-mini (or your custom model) sees your image preview
39
+ 2. **Context Integration**: Considers query + metadata + candidates
40
+ 3. **Reasoning**: Explains why each tool matches
41
+ 4. **Scoring**: Assigns accuracy scores (0-100%)
42
+ 5. **Ranking**: Orders tools by relevance
43
+
44
+ **Single VLM call** - comprehensive analysis with explanations.
45
+
46
+ ## Recommendation Format
47
+
48
+ Each recommendation includes several components:
49
+
50
+ ### Header Information
51
+
52
+ #### Rank Number
53
+ Position in the ranked list (1 = best match).
54
+
55
+ ```
56
+ 1️⃣ TotalSegmentator
57
+ 2️⃣ MedSAM
58
+ 3️⃣ nnU-Net
59
+ ```
60
+
61
+ #### Tool Name
62
+ The software or tool identifier, typically matching:
63
+ - GitHub repository name
64
+ - Published tool name
65
+ - Common community name
66
+
67
+ #### Accuracy Score
68
+ Confidence level from 0-100%:
69
+
70
+ - **90-100%**: Excellent match, highly confident
71
+ - **70-89%**: Good match, suitable for task
72
+ - **50-69%**: Moderate match, may need adaptation
73
+ - **Below 50%**: Weak match, alternative approach
74
+
75
+ !!! note "Score Interpretation"
76
+ Scores reflect match quality for **your specific task and image**, not overall tool quality.
77
+
78
+ ### Body Content
79
+
80
+ #### Description
81
+ Brief explanation of what the tool does:
82
+
83
+ ```
84
+ TotalSegmentator: Automated multi-organ segmentation for CT scans supporting 104 anatomical structures.
85
+ ```
86
+
87
+ #### Explanation
88
+ Why this tool matches your request:
89
+
90
+ ```
91
+ Explanation: TotalSegmentator is specifically designed for whole-body CT segmentation including lung structures. It supports DICOM input and provides automated, accurate lung segmentation without manual intervention.
92
+ ```
93
+
94
+ Key points in explanations:
95
+
96
+ - **Task Alignment**: How well it matches your goal
97
+ - **Format Compatibility**: Support for your file format
98
+ - **Relevant Features**: Specific capabilities that help
99
+ - **Known Limitations**: Caveats or requirements
100
+
101
+ #### Demo Link
102
+ Direct link to a runnable example:
103
+
104
+ ```
105
+ 🚀 Demo: https://huggingface.co/spaces/example/totalsegmentator
106
+ ```
107
+
108
+ Types of demos:
109
+
110
+ - **HuggingFace Spaces**: Interactive Gradio/Streamlit apps
111
+ - **Colab Notebooks**: Jupyter notebooks you can run
112
+ - **Web Demos**: Hosted web interfaces
113
+ - **Documentation**: GitHub README with examples
114
+
115
+ ### Metadata Footer
116
+
117
+ Technical details about the tool:
118
+
119
+ #### Modality Support
120
+ Medical imaging modalities the tool works with:
121
+
122
+ ```
123
+ Modalities: CT, MRI, X-ray
124
+ ```
125
+
126
+ Common modalities:
127
+
128
+ - **CT**: Computed Tomography
129
+ - **MRI**: Magnetic Resonance Imaging
130
+ - **XR**: X-ray radiography
131
+ - **US**: Ultrasound
132
+ - **PET**: Positron Emission Tomography
133
+ - **OCT**: Optical Coherence Tomography
134
+ - **Microscopy**: Various microscopy types
135
+
136
+ #### Dimension Support
137
+ Image/volume dimensions supported:
138
+
139
+ ```
140
+ Dimensions: 2D, 3D
141
+ ```
142
+
143
+ - **2D**: Single slice images
144
+ - **3D**: Volumetric data
145
+ - **4D**: Time-series volumes
146
+
147
+ #### Format Support
148
+ File formats the tool can process:
149
+
150
+ ```
151
+ Formats: DICOM, NIfTI, PNG, JPEG
152
+ ```
153
+
154
+ !!! tip "Format Importance"
155
+ Tools that support your **exact format** are prioritized in ranking.
156
+
157
+ #### Tags
158
+ Categorization and keywords:
159
+
160
+ ```
161
+ Tags: segmentation, medical-imaging, deep-learning, pytorch
162
+ ```
163
+
164
+ Used for:
165
+ - Task categorization
166
+ - Technology stack
167
+ - Domain specificity
168
+ - Feature indicators
169
+
170
+ ## Scoring Factors
171
+
172
+ The agent considers multiple factors when scoring:
173
+
174
+ ### Primary Factors (High Weight)
175
+
176
+ 1. **Task Match**: How well the tool's purpose aligns with your request
177
+ 2. **Format Compatibility**: Support for your input format
178
+ 3. **Image Content**: Visual analysis of what's in your image
179
+ 4. **Dimension Match**: 2D tool for 2D images, 3D for volumes
180
+
181
+ ### Secondary Factors (Medium Weight)
182
+
183
+ 5. **Modality Specificity**: Tool designed for your imaging modality
184
+ 6. **Feature Coverage**: Breadth of capabilities
185
+ 7. **Stated Requirements**: Meets any specific requirements you mentioned
186
+ 8. **Quality Indicators**: Stars, citations, community adoption
187
+
188
+ ### Tertiary Factors (Low Weight)
189
+
190
+ 9. **License**: Open-source vs. proprietary
191
+ 10. **Recency**: Recently updated tools
192
+ 11. **Documentation Quality**: Demo availability, examples
193
+ 12. **Popularity**: Community usage and validation
194
+
195
+ ## Interpreting Results
196
+
197
+ ### High-Scoring Recommendations
198
+
199
+ When you see scores above 85%:
200
+
201
+ ✅ **Strong match** - Tool is designed for this task
202
+ ✅ **Format compatible** - Handles your file type
203
+ ✅ **Proven capability** - Demonstrated results in this domain
204
+
205
+ **Action**: These are your best options. Try the top recommendation first.
206
+
207
+ ### Medium-Scoring Recommendations
208
+
209
+ Scores 60-85%:
210
+
211
+ ⚠️ **Good match** - Suitable but may need adaptation
212
+ ⚠️ **Possible format conversion** - Might require preprocessing
213
+ ⚠️ **Partial capability** - Covers some but not all requirements
214
+
215
+ **Action**: Worth trying, especially if top choices don't work. Read explanations carefully.
216
+
217
+ ### Low-Scoring Recommendations
218
+
219
+ Scores below 60%:
220
+
221
+ ❌ **Weak match** - Limited alignment with task
222
+ ❌ **Format issues** - May not support your format
223
+ ❌ **Alternative approach** - Different methodology
224
+
225
+ **Action**: Consider as fallback or for exploring alternative approaches.
226
+
227
+ ## Why Rankings Change
228
+
229
+ Rankings depend on your specific context:
230
+
231
+ ### Same Tool, Different Queries
232
+
233
+ "Segment lungs" vs "Detect tumors":
234
+ - Different tools excel at each task
235
+ - Rankings change based on task specificity
236
+
237
+ ### Same Task, Different Formats
238
+
239
+ DICOM input vs PNG input:
240
+ - DICOM-compatible tools rank higher for DICOM
241
+ - General tools rank higher for standard images
242
+
243
+ ### Same Task, Different Images
244
+
245
+ CT scan vs X-ray:
246
+ - Modality-specific tools get boosted
247
+ - Visual content influences selection
248
+
249
+ ## Common Patterns
250
+
251
+ ### All High Scores
252
+ Most recommendations >80%:
253
+
254
+ - **Good news!** Multiple excellent options
255
+ - **Strategy**: Try top recommendation, then compare
256
+
257
+ ### Mixed Scores
258
+ Wide range (e.g., 90%, 65%, 45%):
259
+
260
+ - **Top choice clear** - Focus on highest scorer
261
+ - **Strategy**: Try #1, fall back to #2 if needed
262
+
263
+ ### All Low Scores
264
+ All recommendations <60%:
265
+
266
+ - **Limited options** - Task may be specialized
267
+ - **Strategy**: Try anyway, or rephrase query
268
+ - **Alternative**: Ask for suggestions
269
+
270
+ ## Acting on Recommendations
271
+
272
+ ### First Time with a Tool
273
+
274
+ 1. **Read the explanation** - Understand why it was recommended
275
+ 2. **Check format compatibility** - Verify it supports your format
276
+ 3. **Click demo link** - See it in action
277
+ 4. **Try on your data** - Run if agent offers
278
+
279
+ ### Comparing Tools
280
+
281
+ When choosing between similar scores:
282
+
283
+ - **Check licenses** if redistribution matters
284
+ - **Compare formats** - prefer exact format match
285
+ - **Review tags** - match technology preferences
286
+ - **Demo availability** - easier to try
287
+
288
+ ### When Results Don't Match
289
+
290
+ If recommendations seem wrong:
291
+
292
+ 1. **Provide more context**: "I need 3D volume support"
293
+ 2. **Mention specific requirements**: "Must work with DICOM"
294
+ 3. **Exclude irrelevant tools**: `[EXCLUDE:toolname]`
295
+ 4. **Request alternatives**: "Can you search differently?"
296
+
297
+ ## Explanation Analysis
298
+
299
+ Read explanations to understand:
300
+
301
+ ### Positive Indicators
302
+
303
+ Look for phrases like:
304
+ - "Specifically designed for..."
305
+ - "Supports your exact format..."
306
+ - "Demonstrated accuracy on..."
307
+ - "Active development and maintained"
308
+
309
+ ### Caveats
310
+
311
+ Watch for:
312
+ - "May require preprocessing..."
313
+ - "Limited to 2D images..."
314
+ - "Experimental feature..."
315
+ - "Requires specific environment..."
316
+
317
+ ### Requirements
318
+
319
+ Note when explanations mention:
320
+ - "Needs GPU for inference"
321
+ - "Requires Python 3.8+"
322
+ - "DICOM headers must include..."
323
+ - "Minimum image resolution..."
324
+
325
+ ## Next Steps
326
+
327
+ - Learn about [Running Demos](running-demos.md)
328
+ - Explore [Advanced Features](advanced-features.md)
329
+ - Understand the [Architecture Overview](../architecture/overview.md)
docs/user-guide/running-demos.md CHANGED
@@ -1,372 +1,372 @@
1
- # Running Demos (This area is still under construction..)
2
-
3
- The AI Imaging Agent can execute tool demos directly on your uploaded images. This guide explains how demo execution works and how to use it effectively.
4
-
5
- ## What Are Demos?
6
-
7
- Demos are **runnable examples** of imaging tools, typically hosted as:
8
-
9
- - **HuggingFace Spaces**: Interactive Gradio or Streamlit applications
10
- - **Jupyter Notebooks**: Google Colab or similar notebook environments
11
- - **Web Applications**: Hosted web interfaces
12
- - **GitHub Examples**: Code repositories with example scripts
13
-
14
- ## Demo Execution Flow
15
-
16
- ### 1. Agent Offers to Run Demo
17
-
18
- After providing recommendations, the agent may offer:
19
-
20
- ```
21
- Agent: Would you like me to run the demo with your image?
22
- ```
23
-
24
- This appears when:
25
-
26
- - Tool has a compatible Gradio Space demo
27
- - Your image format is compatible
28
- - Demo's API is accessible
29
-
30
- ### 2. You Confirm
31
-
32
- Respond with affirmative language:
33
-
34
- - "yes"
35
- - "sure"
36
- - "ok"
37
- - "please"
38
- - "go ahead"
39
- - "run it"
40
-
41
- The agent detects these patterns and proceeds.
42
-
43
- ### 3. Execution Happens
44
-
45
- The agent:
46
-
47
- 1. Uploads your image to the demo space
48
- 2. Configures any required parameters
49
- 3. Triggers execution
50
- 4. Monitors progress
51
- 5. Retrieves results
52
-
53
- ### 4. Results Display
54
-
55
- You receive:
56
-
57
- - **Success message** with output
58
- - **Result images** or files
59
- - **Execution trace** showing what happened
60
-
61
- ## Demo Types
62
-
63
- ### Gradio Space Demos
64
-
65
- **Best supported** - Direct API integration:
66
-
67
- ```
68
- 🚀 Demo: https://huggingface.co/spaces/username/toolname
69
- ```
70
-
71
- **Features**:
72
-
73
- - ✅ Automatic execution
74
- - ✅ Progress monitoring
75
- - ✅ Result retrieval
76
- - ✅ Error handling
77
-
78
- **Example**:
79
- ```
80
- Running TotalSegmentator on your CT scan...
81
- ✓ Image uploaded
82
- ✓ Processing started
83
- ✓ Segmentation complete
84
- ✓ Results downloaded
85
- ```
86
-
87
- ### Notebook Demos
88
-
89
- **Partially supported** - Links provided for manual execution:
90
-
91
- ```
92
- 📓 Notebook: https://colab.research.google.com/...
93
- ```
94
-
95
- **Process**:
96
-
97
- 1. Click the notebook link
98
- 2. Open in Google Colab
99
- 3. Upload your image to the notebook
100
- 4. Run cells sequentially
101
- 5. Download results
102
-
103
- ### Web Application Demos
104
-
105
- **Manual execution** - Opens in browser:
106
-
107
- ```
108
- 🌐 Web Demo: https://example.com/tool
109
- ```
110
-
111
- **Process**:
112
-
113
- 1. Click the demo link
114
- 2. Web app opens in new tab
115
- 3. Upload your image via the web UI
116
- 4. Configure settings
117
- 5. Run and download results
118
-
119
- ### GitHub Repository Examples
120
-
121
- **Code-based** - Requires local setup:
122
-
123
- ```
124
- 💻 Repository: https://github.com/user/repo
125
- ```
126
-
127
- **Process**:
128
-
129
- 1. Clone the repository
130
- 2. Install dependencies
131
- 3. Run example scripts
132
- 4. Adapt for your data
133
-
134
- ## Execution Traces
135
-
136
- When demos run, you see detailed traces:
137
-
138
- ```html
139
- <details>
140
- <summary>🔧 Tool Execution Trace</summary>
141
-
142
- Step 1: Uploading image to Gradio Space
143
- ✓ Connected to space: username/toolname
144
- ✓ Image uploaded: 2.3 MB
145
-
146
- Step 2: Configuring parameters
147
- ✓ Set task: lung-segmentation
148
- ✓ Set format: DICOM
149
-
150
- Step 3: Running inference
151
- ⏳ Processing... (estimated 30s)
152
- ✓ Completed in 28s
153
-
154
- Step 4: Retrieving results
155
- ✓ Downloaded segmentation mask: 1.1 MB
156
- ✓ Downloaded visualization: 0.8 MB
157
-
158
- Status: ✅ Success
159
- </details>
160
- ```
161
-
162
- Click to expand and see full details.
163
-
164
- ## Supported Gradio Spaces
165
-
166
- ### Auto-Detected Parameters
167
-
168
- The agent automatically configures:
169
-
170
- #### Image Input
171
- - Detects image input component(s)
172
- - Uploads your file
173
- - Converts format if needed
174
-
175
- #### Task Selection
176
- Common task parameters:
177
-
178
- - **Task dropdown**: Matches your query to task option
179
- - **Model selection**: Chooses appropriate model
180
- - **Mode**: Inference, predict, analyze, etc.
181
-
182
- #### Format Options
183
- - **Input format**: DICOM, NIfTI, PNG, etc.
184
- - **Output format**: Segmentation mask, visualization, etc.
185
- - **Data type**: 2D, 3D, specific modality
186
-
187
- ### Manual Parameters
188
-
189
- Some demos require manual interaction:
190
-
191
- ```
192
- Agent: This demo has additional parameters. Please visit the link to configure:
193
- - Segmentation threshold: 0.5
194
- - Post-processing: enabled
195
- ```
196
-
197
- ## Demo Execution Best Practices
198
-
199
- !!! tip "Check Compatibility First"
200
- Verify the tool supports your file format in the recommendation metadata.
201
-
202
- !!! tip "Use Standard Formats"
203
- Demos work best with standard formats (PNG, JPEG for general; DICOM, NIfTI for medical).
204
-
205
- !!! tip "Be Patient"
206
- Some demos take time, especially for:
207
- - Large images or volumes
208
- - Deep learning models
209
- - 3D processing
210
-
211
- Typical times: 10 seconds to 2 minutes.
212
-
213
- !!! tip "Save Results Immediately"
214
- Download result files promptly - they may not persist after closing the browser.
215
-
216
- !!! warning "Rate Limits"
217
- Public Gradio Spaces may have rate limits or queue systems during high usage.
218
-
219
- ## Troubleshooting Demo Execution
220
-
221
- ### Demo Fails to Run
222
-
223
- **Error**: Connection timeout or failed upload
224
-
225
- **Solutions**:
226
-
227
- - Check internet connection
228
- - Try again (server may be busy)
229
- - Visit demo link manually
230
- - Try alternative recommendation
231
-
232
- ### Wrong Results
233
-
234
- **Error**: Output doesn't match expectations
235
-
236
- **Solutions**:
237
-
238
- - Check if correct parameters were used
239
- - Verify image uploaded correctly
240
- - Try adjusting task settings manually
241
- - Compare with demo's example images
242
-
243
- ### Incompatible Format
244
-
245
- **Error**: "Format not supported"
246
-
247
- **Solutions**:
248
-
249
- - Convert image to supported format
250
- - Use tool that accepts your format
251
- - Try alternative recommendation
252
-
253
- ### Demo Link Broken
254
-
255
- **Error**: 404 or space not found
256
-
257
- **Solutions**:
258
-
259
- - Space may be temporarily down
260
- - Check GitHub repo for alternative demo
261
- - Try different tool recommendation
262
- - Report broken link
263
-
264
- ## Manual Demo Execution
265
-
266
- If automatic execution isn't available:
267
-
268
- ### For Gradio Spaces
269
-
270
- 1. Click the demo link
271
- 2. The space opens in your browser
272
- 3. Upload your image via the UI
273
- 4. Select appropriate options
274
- 5. Click "Submit" or "Run"
275
- 6. Download results
276
-
277
- ### For Colab Notebooks
278
-
279
- 1. Click the notebook link
280
- 2. Open in Google Colab
281
- 3. Run setup cells (install dependencies)
282
- 4. Upload your image when prompted:
283
- ```python
284
- from google.colab import files
285
- uploaded = files.upload()
286
- ```
287
- 5. Run processing cells
288
- 6. Download results:
289
- ```python
290
- files.download('result.png')
291
- ```
292
-
293
- ### For Local Execution
294
-
295
- 1. Clone the repository:
296
- ```bash
297
- git clone https://github.com/user/repo
298
- cd repo
299
- ```
300
-
301
- 2. Install dependencies:
302
- ```bash
303
- pip install -r requirements.txt
304
- ```
305
-
306
- 3. Run example script:
307
- ```bash
308
- python run_demo.py --input your_image.png --output result.png
309
- ```
310
-
311
- 4. Check output directory for results
312
-
313
- ## Understanding Results
314
-
315
- ### Segmentation Results
316
-
317
- Typically includes:
318
-
319
- - **Segmentation mask**: Binary or multi-class mask
320
- - **Overlay visualization**: Mask overlaid on original image
321
- - **Statistics**: Volume, area, counts
322
-
323
- ### Detection Results
324
-
325
- Usually provides:
326
-
327
- - **Bounding boxes**: Coordinates of detected objects
328
- - **Annotated image**: Visual with boxes/labels
329
- - **Confidence scores**: Detection confidence
330
-
331
- ### Registration Results
332
-
333
- Common outputs:
334
-
335
- - **Transformed image**: Registered/aligned image
336
- - **Transformation matrix**: Spatial transform parameters
337
- - **Quality metrics**: Similarity scores
338
-
339
- ### Classification Results
340
-
341
- Typical outputs:
342
-
343
- - **Class labels**: Predicted categories
344
- - **Probabilities**: Confidence per class
345
- - **Visualization**: Class activation maps
346
-
347
- ## Demo Feedback
348
-
349
- Help improve the agent by reporting:
350
-
351
- ### Successful Demos
352
- When demos work well, this validates:
353
-
354
- - Tool compatibility
355
- - Parameter auto-configuration
356
- - Format handling
357
-
358
- ### Issues
359
- Report when:
360
-
361
- - Demo fails unexpectedly
362
- - Results are incorrect
363
- - Parameters were misconfigured
364
- - Format conversion was wrong
365
-
366
- Feedback helps refine the agent's demo execution capabilities.
367
-
368
- ## Next Steps
369
-
370
- - Explore [Advanced Features](advanced-features.md)
371
- - Learn about the [Architecture](../architecture/overview.md)
372
- - Check [CLI Commands](../reference/cli.md)
 
1
+ # Running Demos (This area is still under construction..)
2
+
3
+ The AI Imaging Agent can execute tool demos directly on your uploaded images. This guide explains how demo execution works and how to use it effectively.
4
+
5
+ ## What Are Demos?
6
+
7
+ Demos are **runnable examples** of imaging tools, typically hosted as:
8
+
9
+ - **HuggingFace Spaces**: Interactive Gradio or Streamlit applications
10
+ - **Jupyter Notebooks**: Google Colab or similar notebook environments
11
+ - **Web Applications**: Hosted web interfaces
12
+ - **GitHub Examples**: Code repositories with example scripts
13
+
14
+ ## Demo Execution Flow
15
+
16
+ ### 1. Agent Offers to Run Demo
17
+
18
+ After providing recommendations, the agent may offer:
19
+
20
+ ```
21
+ Agent: Would you like me to run the demo with your image?
22
+ ```
23
+
24
+ This appears when:
25
+
26
+ - Tool has a compatible Gradio Space demo
27
+ - Your image format is compatible
28
+ - Demo's API is accessible
29
+
30
+ ### 2. You Confirm
31
+
32
+ Respond with affirmative language:
33
+
34
+ - "yes"
35
+ - "sure"
36
+ - "ok"
37
+ - "please"
38
+ - "go ahead"
39
+ - "run it"
40
+
41
+ The agent detects these patterns and proceeds.
42
+
43
+ ### 3. Execution Happens
44
+
45
+ The agent:
46
+
47
+ 1. Uploads your image to the demo space
48
+ 2. Configures any required parameters
49
+ 3. Triggers execution
50
+ 4. Monitors progress
51
+ 5. Retrieves results
52
+
53
+ ### 4. Results Display
54
+
55
+ You receive:
56
+
57
+ - **Success message** with output
58
+ - **Result images** or files
59
+ - **Execution trace** showing what happened
60
+
61
+ ## Demo Types
62
+
63
+ ### Gradio Space Demos
64
+
65
+ **Best supported** - Direct API integration:
66
+
67
+ ```
68
+ 🚀 Demo: https://huggingface.co/spaces/username/toolname
69
+ ```
70
+
71
+ **Features**:
72
+
73
+ - ✅ Automatic execution
74
+ - ✅ Progress monitoring
75
+ - ✅ Result retrieval
76
+ - ✅ Error handling
77
+
78
+ **Example**:
79
+ ```
80
+ Running TotalSegmentator on your CT scan...
81
+ ✓ Image uploaded
82
+ ✓ Processing started
83
+ ✓ Segmentation complete
84
+ ✓ Results downloaded
85
+ ```
86
+
87
+ ### Notebook Demos
88
+
89
+ **Partially supported** - Links provided for manual execution:
90
+
91
+ ```
92
+ 📓 Notebook: https://colab.research.google.com/...
93
+ ```
94
+
95
+ **Process**:
96
+
97
+ 1. Click the notebook link
98
+ 2. Open in Google Colab
99
+ 3. Upload your image to the notebook
100
+ 4. Run cells sequentially
101
+ 5. Download results
102
+
103
+ ### Web Application Demos
104
+
105
+ **Manual execution** - Opens in browser:
106
+
107
+ ```
108
+ 🌐 Web Demo: https://example.com/tool
109
+ ```
110
+
111
+ **Process**:
112
+
113
+ 1. Click the demo link
114
+ 2. Web app opens in new tab
115
+ 3. Upload your image via the web UI
116
+ 4. Configure settings
117
+ 5. Run and download results
118
+
119
+ ### GitHub Repository Examples
120
+
121
+ **Code-based** - Requires local setup:
122
+
123
+ ```
124
+ 💻 Repository: https://github.com/user/repo
125
+ ```
126
+
127
+ **Process**:
128
+
129
+ 1. Clone the repository
130
+ 2. Install dependencies
131
+ 3. Run example scripts
132
+ 4. Adapt for your data
133
+
134
+ ## Execution Traces
135
+
136
+ When demos run, you see detailed traces:
137
+
138
+ ```html
139
+ <details>
140
+ <summary>🔧 Tool Execution Trace</summary>
141
+
142
+ Step 1: Uploading image to Gradio Space
143
+ ✓ Connected to space: username/toolname
144
+ ✓ Image uploaded: 2.3 MB
145
+
146
+ Step 2: Configuring parameters
147
+ ✓ Set task: lung-segmentation
148
+ ✓ Set format: DICOM
149
+
150
+ Step 3: Running inference
151
+ ⏳ Processing... (estimated 30s)
152
+ ✓ Completed in 28s
153
+
154
+ Step 4: Retrieving results
155
+ ✓ Downloaded segmentation mask: 1.1 MB
156
+ ✓ Downloaded visualization: 0.8 MB
157
+
158
+ Status: ✅ Success
159
+ </details>
160
+ ```
161
+
162
+ Click to expand and see full details.
163
+
164
+ ## Supported Gradio Spaces
165
+
166
+ ### Auto-Detected Parameters
167
+
168
+ The agent automatically configures:
169
+
170
+ #### Image Input
171
+ - Detects image input component(s)
172
+ - Uploads your file
173
+ - Converts format if needed
174
+
175
+ #### Task Selection
176
+ Common task parameters:
177
+
178
+ - **Task dropdown**: Matches your query to task option
179
+ - **Model selection**: Chooses appropriate model
180
+ - **Mode**: Inference, predict, analyze, etc.
181
+
182
+ #### Format Options
183
+ - **Input format**: DICOM, NIfTI, PNG, etc.
184
+ - **Output format**: Segmentation mask, visualization, etc.
185
+ - **Data type**: 2D, 3D, specific modality
186
+
187
+ ### Manual Parameters
188
+
189
+ Some demos require manual interaction:
190
+
191
+ ```
192
+ Agent: This demo has additional parameters. Please visit the link to configure:
193
+ - Segmentation threshold: 0.5
194
+ - Post-processing: enabled
195
+ ```
196
+
197
+ ## Demo Execution Best Practices
198
+
199
+ !!! tip "Check Compatibility First"
200
+ Verify the tool supports your file format in the recommendation metadata.
201
+
202
+ !!! tip "Use Standard Formats"
203
+ Demos work best with standard formats (PNG, JPEG for general; DICOM, NIfTI for medical).
204
+
205
+ !!! tip "Be Patient"
206
+ Some demos take time, especially for:
207
+ - Large images or volumes
208
+ - Deep learning models
209
+ - 3D processing
210
+
211
+ Typical times: 10 seconds to 2 minutes.
212
+
213
+ !!! tip "Save Results Immediately"
214
+ Download result files promptly - they may not persist after closing the browser.
215
+
216
+ !!! warning "Rate Limits"
217
+ Public Gradio Spaces may have rate limits or queue systems during high usage.
218
+
219
+ ## Troubleshooting Demo Execution
220
+
221
+ ### Demo Fails to Run
222
+
223
+ **Error**: Connection timeout or failed upload
224
+
225
+ **Solutions**:
226
+
227
+ - Check internet connection
228
+ - Try again (server may be busy)
229
+ - Visit demo link manually
230
+ - Try alternative recommendation
231
+
232
+ ### Wrong Results
233
+
234
+ **Error**: Output doesn't match expectations
235
+
236
+ **Solutions**:
237
+
238
+ - Check if correct parameters were used
239
+ - Verify image uploaded correctly
240
+ - Try adjusting task settings manually
241
+ - Compare with demo's example images
242
+
243
+ ### Incompatible Format
244
+
245
+ **Error**: "Format not supported"
246
+
247
+ **Solutions**:
248
+
249
+ - Convert image to supported format
250
+ - Use tool that accepts your format
251
+ - Try alternative recommendation
252
+
253
+ ### Demo Link Broken
254
+
255
+ **Error**: 404 or space not found
256
+
257
+ **Solutions**:
258
+
259
+ - Space may be temporarily down
260
+ - Check GitHub repo for alternative demo
261
+ - Try different tool recommendation
262
+ - Report broken link
263
+
264
+ ## Manual Demo Execution
265
+
266
+ If automatic execution isn't available:
267
+
268
+ ### For Gradio Spaces
269
+
270
+ 1. Click the demo link
271
+ 2. The space opens in your browser
272
+ 3. Upload your image via the UI
273
+ 4. Select appropriate options
274
+ 5. Click "Submit" or "Run"
275
+ 6. Download results
276
+
277
+ ### For Colab Notebooks
278
+
279
+ 1. Click the notebook link
280
+ 2. Open in Google Colab
281
+ 3. Run setup cells (install dependencies)
282
+ 4. Upload your image when prompted:
283
+ ```python
284
+ from google.colab import files
285
+ uploaded = files.upload()
286
+ ```
287
+ 5. Run processing cells
288
+ 6. Download results:
289
+ ```python
290
+ files.download('result.png')
291
+ ```
292
+
293
+ ### For Local Execution
294
+
295
+ 1. Clone the repository:
296
+ ```bash
297
+ git clone https://github.com/user/repo
298
+ cd repo
299
+ ```
300
+
301
+ 2. Install dependencies:
302
+ ```bash
303
+ pip install -r requirements.txt
304
+ ```
305
+
306
+ 3. Run example script:
307
+ ```bash
308
+ python run_demo.py --input your_image.png --output result.png
309
+ ```
310
+
311
+ 4. Check output directory for results
312
+
313
+ ## Understanding Results
314
+
315
+ ### Segmentation Results
316
+
317
+ Typically includes:
318
+
319
+ - **Segmentation mask**: Binary or multi-class mask
320
+ - **Overlay visualization**: Mask overlaid on original image
321
+ - **Statistics**: Volume, area, counts
322
+
323
+ ### Detection Results
324
+
325
+ Usually provides:
326
+
327
+ - **Bounding boxes**: Coordinates of detected objects
328
+ - **Annotated image**: Visual with boxes/labels
329
+ - **Confidence scores**: Detection confidence
330
+
331
+ ### Registration Results
332
+
333
+ Common outputs:
334
+
335
+ - **Transformed image**: Registered/aligned image
336
+ - **Transformation matrix**: Spatial transform parameters
337
+ - **Quality metrics**: Similarity scores
338
+
339
+ ### Classification Results
340
+
341
+ Typical outputs:
342
+
343
+ - **Class labels**: Predicted categories
344
+ - **Probabilities**: Confidence per class
345
+ - **Visualization**: Class activation maps
346
+
347
+ ## Demo Feedback
348
+
349
+ Help improve the agent by reporting:
350
+
351
+ ### Successful Demos
352
+ When demos work well, this validates:
353
+
354
+ - Tool compatibility
355
+ - Parameter auto-configuration
356
+ - Format handling
357
+
358
+ ### Issues
359
+ Report when:
360
+
361
+ - Demo fails unexpectedly
362
+ - Results are incorrect
363
+ - Parameters were misconfigured
364
+ - Format conversion was wrong
365
+
366
+ Feedback helps refine the agent's demo execution capabilities.
367
+
368
+ ## Next Steps
369
+
370
+ - Explore [Advanced Features](advanced-features.md)
371
+ - Learn about the [Architecture](../architecture/overview.md)
372
+ - Check [CLI Commands](../reference/cli.md)
mkdocs.yml CHANGED
@@ -1,108 +1,108 @@
1
- site_name: AI Imaging Agent
2
- site_description: Intelligent RAG + AI agent for discovering imaging software
3
- site_author: Imaging Plaza
4
- site_url: https://imaging-plaza.github.io/ai-agent/
5
-
6
- repo_name: imaging-plaza/ai-agent
7
- repo_url: https://github.com/imaging-plaza/ai-agent
8
- edit_uri: edit/main/docs/
9
-
10
- theme:
11
- name: material
12
- palette:
13
- # Palette toggle for light mode
14
- - scheme: default
15
- primary: teal
16
- accent: green
17
- toggle:
18
- icon: material/brightness-7
19
- name: Switch to dark mode
20
- # Palette toggle for dark mode
21
- - scheme: slate
22
- primary: teal
23
- accent: green
24
- toggle:
25
- icon: material/brightness-4
26
- name: Switch to light mode
27
-
28
- font:
29
- text: Roboto
30
- code: Roboto Mono
31
-
32
- features:
33
- - navigation.instant
34
- - navigation.tracking
35
- - navigation.tabs
36
- - navigation.sections
37
- - navigation.top
38
- - navigation.footer
39
- - search.suggest
40
- - search.highlight
41
- - content.code.copy
42
- - content.code.annotate
43
- - content.tabs.link
44
-
45
- icon:
46
- repo: fontawesome/brands/github
47
-
48
- plugins:
49
- - search
50
- - tags
51
-
52
- markdown_extensions:
53
- - admonition
54
- - pymdownx.details
55
- - pymdownx.superfences:
56
- custom_fences:
57
- - name: mermaid
58
- class: mermaid
59
- format: !!python/name:pymdownx.superfences.fence_code_format
60
- - pymdownx.tabbed:
61
- alternate_style: true
62
- - pymdownx.highlight:
63
- anchor_linenums: true
64
- - pymdownx.inlinehilite
65
- - pymdownx.snippets
66
- - attr_list
67
- - md_in_html
68
- - pymdownx.emoji:
69
- emoji_index: !!python/name:material.extensions.emoji.twemoji
70
- emoji_generator: !!python/name:material.extensions.emoji.to_svg
71
- - tables
72
- - footnotes
73
- - toc:
74
- permalink: true
75
-
76
- nav:
77
- - Home: index.md
78
- - Getting Started:
79
- - Installation: getting-started/installation.md
80
- - Quick Start: getting-started/quickstart.md
81
- - Configuration: getting-started/configuration.md
82
- - User Guide:
83
- - Using the Chat Interface: user-guide/chat-interface.md
84
- - Supported File Formats: user-guide/file-formats.md
85
- - Understanding Recommendations: user-guide/recommendations.md
86
- - Running Demos: user-guide/running-demos.md
87
- - Advanced Features: user-guide/advanced-features.md
88
- - Architecture:
89
- - Overview: architecture/overview.md
90
- - Retrieval Pipeline: architecture/retrieval.md
91
- - Agent & VLM Selection: architecture/agent.md
92
- - Software Catalog: architecture/catalog.md
93
- - Development:
94
- - Project Guide: guide.md
95
- - Project Structure: development/structure.md
96
- - Contributing: development/contributing.md
97
- - Testing: development/testing.md
98
- - Reference:
99
- - CLI Commands: reference/cli.md
100
- - Environment Variables: reference/environment.md
101
- - Changelog: reference/changelog.md
102
-
103
- extra:
104
- social:
105
- - icon: fontawesome/brands/github
106
- link: https://github.com/imaging-plaza/ai-agent
107
-
108
- copyright: Copyright &copy; 2024-2026 Imaging Plaza
 
1
+ site_name: AI Imaging Agent
2
+ site_description: Intelligent RAG + AI agent for discovering imaging software
3
+ site_author: Imaging Plaza
4
+ site_url: https://imaging-plaza.github.io/ai-agent/
5
+
6
+ repo_name: imaging-plaza/ai-agent
7
+ repo_url: https://github.com/imaging-plaza/ai-agent
8
+ edit_uri: edit/main/docs/
9
+
10
+ theme:
11
+ name: material
12
+ palette:
13
+ # Palette toggle for light mode
14
+ - scheme: default
15
+ primary: teal
16
+ accent: green
17
+ toggle:
18
+ icon: material/brightness-7
19
+ name: Switch to dark mode
20
+ # Palette toggle for dark mode
21
+ - scheme: slate
22
+ primary: teal
23
+ accent: green
24
+ toggle:
25
+ icon: material/brightness-4
26
+ name: Switch to light mode
27
+
28
+ font:
29
+ text: Roboto
30
+ code: Roboto Mono
31
+
32
+ features:
33
+ - navigation.instant
34
+ - navigation.tracking
35
+ - navigation.tabs
36
+ - navigation.sections
37
+ - navigation.top
38
+ - navigation.footer
39
+ - search.suggest
40
+ - search.highlight
41
+ - content.code.copy
42
+ - content.code.annotate
43
+ - content.tabs.link
44
+
45
+ icon:
46
+ repo: fontawesome/brands/github
47
+
48
+ plugins:
49
+ - search
50
+ - tags
51
+
52
+ markdown_extensions:
53
+ - admonition
54
+ - pymdownx.details
55
+ - pymdownx.superfences:
56
+ custom_fences:
57
+ - name: mermaid
58
+ class: mermaid
59
+ format: !!python/name:pymdownx.superfences.fence_code_format
60
+ - pymdownx.tabbed:
61
+ alternate_style: true
62
+ - pymdownx.highlight:
63
+ anchor_linenums: true
64
+ - pymdownx.inlinehilite
65
+ - pymdownx.snippets
66
+ - attr_list
67
+ - md_in_html
68
+ - pymdownx.emoji:
69
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
70
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
71
+ - tables
72
+ - footnotes
73
+ - toc:
74
+ permalink: true
75
+
76
+ nav:
77
+ - Home: index.md
78
+ - Getting Started:
79
+ - Installation: getting-started/installation.md
80
+ - Quick Start: getting-started/quickstart.md
81
+ - Configuration: getting-started/configuration.md
82
+ - User Guide:
83
+ - Using the Chat Interface: user-guide/chat-interface.md
84
+ - Supported File Formats: user-guide/file-formats.md
85
+ - Understanding Recommendations: user-guide/recommendations.md
86
+ - Running Demos: user-guide/running-demos.md
87
+ - Advanced Features: user-guide/advanced-features.md
88
+ - Architecture:
89
+ - Overview: architecture/overview.md
90
+ - Retrieval Pipeline: architecture/retrieval.md
91
+ - Agent & VLM Selection: architecture/agent.md
92
+ - Software Catalog: architecture/catalog.md
93
+ - Development:
94
+ - Project Guide: guide.md
95
+ - Project Structure: development/structure.md
96
+ - Contributing: development/contributing.md
97
+ - Testing: development/testing.md
98
+ - Reference:
99
+ - CLI Commands: reference/cli.md
100
+ - Environment Variables: reference/environment.md
101
+ - Changelog: reference/changelog.md
102
+
103
+ extra:
104
+ social:
105
+ - icon: fontawesome/brands/github
106
+ link: https://github.com/imaging-plaza/ai-agent
107
+
108
+ copyright: Copyright &copy; 2024-2026 Imaging Plaza
pyproject.toml CHANGED
@@ -15,6 +15,10 @@ dependencies = [
15
  "requests==2.32.4",
16
  "python-dotenv==1.1.1",
17
  "gradio==5.42.0",
 
 
 
 
18
  "json5==0.12.0",
19
  "pillow==11.3.0",
20
  "nibabel==5.3.2",
 
15
  "requests==2.32.4",
16
  "python-dotenv==1.1.1",
17
  "gradio==5.42.0",
18
+ "fastapi==0.118.0",
19
+ "uvicorn[standard]==0.36.0",
20
+ "sse-starlette==2.1.3",
21
+ "python-multipart==0.0.20",
22
  "json5==0.12.0",
23
  "pillow==11.3.0",
24
  "nibabel==5.3.2",
src/ai_agent/agent/agent.py CHANGED
@@ -1,611 +1,650 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import logging
5
- import time
6
- import asyncio
7
- import threading
8
- from collections import OrderedDict
9
- from datetime import datetime
10
- from typing import List
11
-
12
- from pydantic_ai import Agent, RunContext
13
- from pydantic_ai.usage import UsageLimits
14
- from pydantic_ai.models.openai import OpenAIResponsesModel, OpenAIChatModel
15
- from pydantic_ai.providers.openai import OpenAIProvider
16
- from pydantic_ai.messages import BinaryContent
17
-
18
- from ai_agent.generator.prompts import get_agent_system_prompt
19
- from ai_agent.generator.schema import ToolSelection, Conversation, ConversationStatus
20
- from ai_agent.utils.config import get_config
21
- from .models import AgentToolSelection, ToolRunLog, UsageStats
22
- from .tools.repo_info_tool import tool_repo_summary, RepoSummaryInput
23
- from ai_agent.agent.utils import coerce_github_url_or_none
24
- from .tools.search_tool import tool_search_tools, SearchToolsInput
25
- from .tools.search_alternative_tool import (
26
- tool_search_alternative,
27
- SearchAlternativeInput,
28
- )
29
- from .tools.query_utils import sanitize_retrieval_query
30
- from .utils import AgentState, limit_tool_calls, cap_prepare
31
- from ai_agent.utils.image_meta import summarize_image_metadata, detect_ext_token
32
-
33
- log = logging.getLogger("agent.core")
34
-
35
- DEFAULT_NUM_CHOICES = int(os.getenv("NUM_CHOICES", "3"))
36
-
37
- # ---------------------------------------------------------------------------
38
- # Dynamic agent instance cache
39
- # Key: (model_name, base_url, api_key_env, num_choices)
40
- # Avoids rebuilding Agent/OpenAIProvider/model objects on every request when
41
- # the UI repeatedly uses the same custom endpoint + model combination.
42
- # Bounded LRU (max AGENT_CACHE_MAX entries); protected by a lock so that
43
- # concurrent requests cannot race while creating/inserting agents.
44
- # ---------------------------------------------------------------------------
45
- _AGENT_CACHE_MAX: int = int(os.getenv("AGENT_CACHE_MAX", "16"))
46
- _AGENT_CACHE_LOCK: threading.Lock = threading.Lock()
47
- _AGENT_CACHE: OrderedDict[tuple, "Agent"] = OrderedDict()
48
-
49
- # ---------------------------------------------------------------------------
50
- # Model / provider setup
51
- # ---------------------------------------------------------------------------
52
- config = get_config()
53
- agent_model_config = config.agent_model
54
-
55
- try:
56
- api_key = agent_model_config.get_api_key()
57
- except ValueError as e:
58
- log.error(f"Failed to get API key for agent model: {e}")
59
- raise
60
-
61
- log.info(f"Initializing agent model: {agent_model_config.name}")
62
-
63
- if agent_model_config.base_url:
64
- log.info(f"Using custom OpenAI base URL: {agent_model_config.base_url}")
65
- log.info("Using OpenAIChatModel (chat/completions API) for custom endpoint")
66
- provider = OpenAIProvider(
67
- base_url=agent_model_config.base_url,
68
- api_key=api_key,
69
- )
70
- openai_model = OpenAIChatModel(
71
- model_name=agent_model_config.name,
72
- provider=provider,
73
- )
74
- else:
75
- provider = OpenAIProvider(api_key=api_key)
76
- openai_model = OpenAIResponsesModel(
77
- model_name=agent_model_config.name,
78
- provider=provider,
79
- )
80
-
81
- # ---------------------------------------------------------------------------
82
- # Agent definition
83
- # ---------------------------------------------------------------------------
84
- agent = Agent(
85
- model=openai_model,
86
- system_prompt=get_agent_system_prompt(DEFAULT_NUM_CHOICES),
87
- deps_type=AgentState,
88
- output_retries=int(os.getenv("AGENT_OUTPUT_RETRIES", "3")),
89
- )
90
-
91
- # ---------------------------------------------------------------------------
92
- # Tool adapters for the agent
93
- # ---------------------------------------------------------------------------
94
-
95
-
96
- @agent.tool(retries=2, prepare=cap_prepare)
97
- @limit_tool_calls("search_tools", cap=1)
98
- async def search_tools(
99
- ctx: RunContext[AgentState],
100
- query: str,
101
- excluded: List[str] | None = None,
102
- top_k: int = 12,
103
- ) -> List[dict]:
104
- """
105
- Agent-facing search tool.
106
-
107
- Delegates to tools.search_tool.tool_search_tools(), but automatically
108
- injects:
109
- - globally excluded tools (from ctx.deps.excluded_tools)
110
- - image_paths and original_formats (from ctx.deps, set in run_agent)
111
- so the language model never has to reason about file paths directly.
112
- """
113
- # Merge explicit exclusions with global exclusions from AgentState
114
- explicit_excluded = excluded or []
115
- global_excluded = getattr(ctx.deps, "excluded_tools", []) or []
116
- all_excluded = sorted(set(explicit_excluded + list(global_excluded)))
117
-
118
- original_formats = getattr(ctx.deps, "original_formats", []) or []
119
- image_paths = getattr(ctx.deps, "image_paths", []) or []
120
-
121
- effective_top_k = (
122
- ctx.deps.override_top_k if ctx.deps.override_top_k is not None else top_k
123
- )
124
-
125
- started = time.perf_counter()
126
- inp = SearchToolsInput(
127
- query=sanitize_retrieval_query(query),
128
- excluded=all_excluded,
129
- top_k=effective_top_k,
130
- original_formats=original_formats,
131
- image_paths=image_paths,
132
- )
133
- out = tool_search_tools(inp)
134
-
135
- ctx.deps.tool_calls.append(
136
- {
137
- "tool": "search_tools",
138
- "query": query,
139
- "count": len(out.candidates),
140
- "duration_ms": round((time.perf_counter() - started) * 1000, 1),
141
- "original_formats": original_formats,
142
- "excluded": all_excluded,
143
- "timestamp": datetime.now().isoformat(),
144
- }
145
- )
146
-
147
- # Return plain dicts so the LLM sees a simple JSON-like structure.
148
- return [c.model_dump(mode="python") for c in out.candidates]
149
-
150
-
151
- @agent.tool(retries=2, prepare=cap_prepare)
152
- @limit_tool_calls("search_alternative", cap=3)
153
- async def search_alternative(
154
- ctx: RunContext[AgentState],
155
- alternative_query: str,
156
- excluded: List[str] | None = None,
157
- top_k: int = 12,
158
- ) -> List[dict]:
159
- """
160
- Search with an alternative query formulation (includes automatic reranking).
161
- """
162
- explicit_excluded = excluded or []
163
- global_excluded = getattr(ctx.deps, "excluded_tools", []) or []
164
- all_excluded = sorted(set(explicit_excluded + list(global_excluded)))
165
-
166
- original_formats = getattr(ctx.deps, "original_formats", []) or []
167
- image_paths = getattr(ctx.deps, "image_paths", []) or []
168
-
169
- started = time.perf_counter()
170
- inp = SearchAlternativeInput(
171
- alternative_query=alternative_query,
172
- excluded=all_excluded,
173
- top_k=top_k,
174
- original_formats=original_formats,
175
- image_paths=image_paths,
176
- )
177
- out = tool_search_alternative(inp)
178
-
179
- ctx.deps.tool_calls.append(
180
- {
181
- "tool": "search_alternative",
182
- "alternative_query": alternative_query,
183
- "query_used": out.query_used,
184
- "count": len(out.candidates),
185
- "duration_ms": round((time.perf_counter() - started) * 1000, 1),
186
- "original_formats": original_formats,
187
- "excluded": all_excluded,
188
- "timestamp": datetime.now().isoformat(),
189
- }
190
- )
191
-
192
- return [c.model_dump(mode="python") for c in out.candidates]
193
-
194
-
195
- @agent.tool(retries=2, prepare=cap_prepare)
196
- @limit_tool_calls("repo_info_batch", cap=4)
197
- async def repo_info_batch(
198
- ctx: RunContext[AgentState],
199
- urls: List[str],
200
- ) -> List[dict]:
201
- """Fetch repository summaries for multiple repositories in parallel."""
202
- started = time.perf_counter()
203
-
204
- if not urls:
205
- return []
206
-
207
- normalized: List[str] = []
208
- skipped: List[dict] = []
209
- seen: set[str] = set()
210
- for raw in urls:
211
- norm = coerce_github_url_or_none(raw)
212
- if not norm:
213
- skipped.append(
214
- {
215
- "url": raw,
216
- "skipped": True,
217
- "reason": "NON_GITHUB_URL",
218
- }
219
- )
220
- continue
221
- if norm in seen:
222
- continue
223
- seen.add(norm)
224
- normalized.append(norm)
225
-
226
- tasks = [tool_repo_summary(RepoSummaryInput(url=u)) for u in normalized]
227
- outcomes = await asyncio.gather(*tasks, return_exceptions=True)
228
-
229
- results: List[dict] = []
230
- for url, outcome in zip(normalized, outcomes):
231
- if isinstance(outcome, Exception):
232
- results.append(
233
- {
234
- "url": url,
235
- "source": "error",
236
- "error": str(outcome),
237
- }
238
- )
239
- continue
240
- payload = outcome.model_dump(mode="python")
241
- payload["url"] = url
242
- results.append(payload)
243
-
244
- if skipped:
245
- results.extend(skipped)
246
-
247
- ctx.deps.tool_calls.append(
248
- {
249
- "tool": "repo_info_batch",
250
- "requested": len(urls),
251
- "normalized": len(normalized),
252
- "returned": len(results),
253
- "duration_ms": round((time.perf_counter() - started) * 1000, 1),
254
- "timestamp": datetime.now().isoformat(),
255
- }
256
- )
257
-
258
- return results
259
-
260
-
261
- # ---------------------------------------------------------------------------
262
- # High level entry point: run the agent on (text query + image)
263
- # ---------------------------------------------------------------------------
264
- def run_agent(
265
- task: str,
266
- image_paths: List[str],
267
- excluded: List[str] | None = None,
268
- conversation_history: List[str] | None = None,
269
- *,
270
- image_bytes: bytes | None = None,
271
- model: str | None = None,
272
- base_url: str | None = None,
273
- api_key_env: str | None = None,
274
- top_k: int | None = None,
275
- num_choices: int | None = None,
276
- image_metadata: str | None = None,
277
- ) -> AgentToolSelection:
278
- """
279
- Execute the agent for a user task and at least one image path.
280
-
281
- - derive canonical original_formats (tiff / dicom / nifti / ...)
282
- - build a compact image metadata summary (or use pre-computed one)
283
- - pass both to the LLM as hidden context
284
- - store image_paths/original_formats in deps so retrieval tools can use them
285
- - optionally allow runtime model/base_url/top_k/num_choices overrides
286
-
287
- IMPORTANT:
288
- The model only sees an actual image if `image_bytes` is provided.
289
- `image_paths` are used for metadata + tool context only.
290
- """
291
- run_started = time.perf_counter()
292
- if not image_paths:
293
- raise ValueError("run_agent requires at least one image path")
294
-
295
- tool_logs: List[ToolRunLog] = []
296
-
297
- # ---- 1) Derive image-based metadata and format hints --------------------
298
- metadata_started = time.perf_counter()
299
- meta_str = (
300
- image_metadata
301
- if image_metadata is not None
302
- else (summarize_image_metadata(image_paths) or "")
303
- )
304
- fmt_str = detect_ext_token(image_paths) or ""
305
- original_formats = [t.lower() for t in fmt_str.split()] if fmt_str else []
306
- metadata_duration_ms = round((time.perf_counter() - metadata_started) * 1000, 1)
307
-
308
- effective_top_k = top_k if top_k is not None else 12
309
- effective_num_choices = num_choices if num_choices is not None else 3
310
-
311
- # ---- 2) Prepare dependency state passed to all tools --------------------
312
- deps = AgentState(
313
- excluded_tools=excluded or [],
314
- override_model=model,
315
- override_base_url=base_url,
316
- override_top_k=effective_top_k,
317
- override_num_choices=effective_num_choices,
318
- )
319
-
320
- setattr(deps, "image_paths", list(image_paths))
321
- setattr(deps, "original_formats", original_formats)
322
-
323
- # ---- 3) Hidden metadata lines for the model ----------------------------
324
- hidden_meta = ""
325
- if original_formats:
326
- hidden_meta += "\n(Formats Hint: " + ",".join(original_formats) + ")"
327
- if meta_str:
328
- short_meta = " ".join(x.strip() for x in meta_str.splitlines() if x.strip())
329
- hidden_meta += (
330
- "\n(Image Metadata: "
331
- + short_meta[:500]
332
- + ("…" if len(short_meta) > 500 else "")
333
- + ")"
334
- )
335
- hidden_meta += f"\n(Search top_k: {effective_top_k})"
336
-
337
- extra_context = "\n\n**CRITICAL: Analyze the attached preview image showing the user's data.**\nUse visual observations (anatomy visible, image quality, dimensionality, contrast) combined with the metadata below to recommend tools. Reference what you see in your explanations."
338
-
339
- # ---- 4) Build the prompt (optionally including history) ----------------
340
- if conversation_history and len(conversation_history) > 0:
341
- history_text = "\n".join(conversation_history)
342
- prompt = (
343
- f"Previous conversation:\n{history_text}\n\n"
344
- f"Current request: {task}{extra_context}{hidden_meta}"
345
- )
346
- else:
347
- prompt = task + extra_context + hidden_meta
348
-
349
- # -----------------------------------------------------------------------
350
- # Determine which agent instance to use
351
- # -----------------------------------------------------------------------
352
- agent_instance = agent
353
- effective_num_choices = num_choices if num_choices is not None else 3
354
- effective_model = model if model else agent_model_config.name
355
- effective_top_k = top_k if top_k is not None else 12
356
-
357
- # When model is provided from UI, base_url comes with it (can be None for OpenAI)
358
- if model:
359
- # Use api_key_env from config if provided, otherwise default to OPENAI_API_KEY
360
- key_env_name = api_key_env if api_key_env else "OPENAI_API_KEY"
361
- runtime_api_key = os.getenv(key_env_name)
362
- if not runtime_api_key:
363
- raise ValueError(
364
- f"{key_env_name} not found in environment. Cannot use this model."
365
- )
366
- effective_base_url = base_url # Can be None for OpenAI
367
- log.info(f" Using {key_env_name} for model {effective_model}")
368
- log.debug(f"{key_env_name} is set: {bool(runtime_api_key)}")
369
- else:
370
- # No model override - use config defaults
371
- effective_base_url = agent_model_config.base_url
372
- runtime_api_key = api_key # Already loaded from config at startup
373
- log.info(f"✓ Using API key from config for model {effective_model}")
374
-
375
- # Log runtime configuration
376
- endpoint_display = effective_base_url if effective_base_url else "api.openai.com"
377
- log.info(
378
- f"🤖 Agent execution - Model: {effective_model}, endpoint: {endpoint_display}, "
379
- f"top_k: {effective_top_k}, num_choices: {effective_num_choices}, excluded: {len(excluded or [])}"
380
- )
381
-
382
- needs_dynamic_agent = model is not None
383
-
384
- if needs_dynamic_agent:
385
- cache_key = (effective_model, effective_base_url or "", api_key_env or "OPENAI_API_KEY", effective_num_choices)
386
- with _AGENT_CACHE_LOCK:
387
- agent_instance = _AGENT_CACHE.get(cache_key)
388
- if agent_instance is not None:
389
- _AGENT_CACHE.move_to_end(cache_key)
390
- if agent_instance is None:
391
- log.info(
392
- f"📦 Creating runtime agent with model={effective_model}, endpoint={effective_base_url or 'api.openai.com'}"
393
- )
394
-
395
- runtime_provider = OpenAIProvider(
396
- base_url=effective_base_url,
397
- api_key=runtime_api_key,
398
- )
399
-
400
- # Use OpenAIChatModel (chat/completions) for custom endpoints, OpenAIResponsesModel for default OpenAI
401
- if effective_base_url:
402
- log.info("Using OpenAIChatModel (chat/completions API) for custom endpoint")
403
- runtime_model = OpenAIChatModel(
404
- model_name=effective_model, provider=runtime_provider
405
- )
406
- else:
407
- runtime_model = OpenAIResponsesModel(
408
- model_name=effective_model, provider=runtime_provider
409
- )
410
-
411
- agent_instance = Agent(
412
- model=runtime_model,
413
- system_prompt=get_agent_system_prompt(effective_num_choices),
414
- deps_type=AgentState,
415
- output_retries=int(os.getenv("AGENT_OUTPUT_RETRIES", "3")),
416
- )
417
-
418
- # Register tools on the dynamic agent
419
- agent_instance.tool(search_tools, retries=2, prepare=cap_prepare)
420
- agent_instance.tool(search_alternative, retries=2, prepare=cap_prepare)
421
- agent_instance.tool(repo_info_batch, retries=2, prepare=cap_prepare)
422
-
423
- with _AGENT_CACHE_LOCK:
424
- _AGENT_CACHE[cache_key] = agent_instance
425
- _AGENT_CACHE.move_to_end(cache_key)
426
- while len(_AGENT_CACHE) > _AGENT_CACHE_MAX:
427
- _AGENT_CACHE.popitem(last=False)
428
- else:
429
- log.info(
430
- f"♻️ Reusing cached dynamic agent (model: {effective_model}, num_choices: {effective_num_choices})"
431
- )
432
-
433
- elif (
434
- num_choices is not None and num_choices != DEFAULT_NUM_CHOICES
435
- ):
436
- cache_key = (effective_model, effective_base_url or "", api_key_env or "OPENAI_API_KEY", effective_num_choices)
437
- with _AGENT_CACHE_LOCK:
438
- agent_instance = _AGENT_CACHE.get(cache_key)
439
- if agent_instance is not None:
440
- _AGENT_CACHE.move_to_end(cache_key)
441
- if agent_instance is None:
442
- log.info(
443
- f"📦 Creating runtime agent with num_choices={effective_num_choices} (model: {effective_model})"
444
- )
445
- agent_instance = Agent(
446
- model=openai_model,
447
- system_prompt=get_agent_system_prompt(effective_num_choices),
448
- deps_type=AgentState,
449
- output_retries=int(os.getenv("AGENT_OUTPUT_RETRIES", "3")),
450
- )
451
-
452
- # Register tools on the dynamic agent
453
- agent_instance.tool(search_tools, retries=2, prepare=cap_prepare)
454
- agent_instance.tool(search_alternative, retries=2, prepare=cap_prepare)
455
- agent_instance.tool(repo_info_batch, retries=2, prepare=cap_prepare)
456
-
457
- with _AGENT_CACHE_LOCK:
458
- _AGENT_CACHE[cache_key] = agent_instance
459
- _AGENT_CACHE.move_to_end(cache_key)
460
- while len(_AGENT_CACHE) > _AGENT_CACHE_MAX:
461
- _AGENT_CACHE.popitem(last=False)
462
- else:
463
- log.info(
464
- f"♻️ Reusing cached dynamic agent with num_choices={effective_num_choices} (model: {effective_model})"
465
- )
466
-
467
- else:
468
- log.info(
469
- f"♻️ Using global agent (model: {effective_model}, num_choices: {effective_num_choices})"
470
- )
471
-
472
- log.debug(
473
- f"Prompt length: {len(prompt)} chars, has_image_paths: {bool(image_paths)}, has_image_bytes: {bool(image_bytes)}"
474
- )
475
-
476
- # ---- 5) Build multimodal prompt if image bytes provided ----------------
477
- if image_bytes:
478
- log.info(
479
- f"🖼️ Sending image preview to model ({len(image_bytes)} bytes = {len(image_bytes)/1024:.1f}KB)"
480
- )
481
- user_prompt = [
482
- prompt,
483
- BinaryContent(
484
- data=image_bytes,
485
- media_type="image/png",
486
- ),
487
- ]
488
- else:
489
- log.warning(
490
- "⚠️ No image bytes provided - the model will not see the image preview"
491
- )
492
- user_prompt = prompt
493
-
494
- # ---- 6) Run the agent --------------------------------------------------
495
- try:
496
- llm_started = time.perf_counter()
497
- run_result = agent_instance.run_sync(
498
- user_prompt,
499
- deps=deps,
500
- output_type=ToolSelection,
501
- usage_limits=UsageLimits(tool_calls_limit=20),
502
- )
503
- llm_duration_ms = round((time.perf_counter() - llm_started) * 1000, 1)
504
- result = run_result.output
505
-
506
- log.info(
507
- f"✅ Agent execution complete - choices returned: {len(result.choices)}"
508
- )
509
-
510
- # Log usage (helpful, but may not explicitly expose image-specific counters)
511
- if run_result.usage:
512
- usage = run_result.usage()
513
- log.info(
514
- f"📊 Usage: total_tokens={usage.total_tokens}, "
515
- f"input_tokens={usage.input_tokens}, output_tokens={usage.output_tokens}"
516
- )
517
-
518
- # Warn if using non-OpenAI endpoint with images
519
- if image_bytes and effective_base_url:
520
- log.warning(
521
- "⚠️ Using custom endpoint - confirm the selected model supports vision."
522
- )
523
-
524
- except Exception as e:
525
- # Handle global tool quota limit (UsageLimitExceeded) and other errors gracefully
526
- error_msg = str(e)
527
- llm_duration_ms = round((time.perf_counter() - llm_started) * 1000, 1)
528
- log.warning(f"⚠️ Agent execution encountered an error: {error_msg}")
529
- run_result = None # Ensure run_result is defined for usage stats extraction
530
-
531
- # Check if this is a usage limit error (global tool quota)
532
- if (
533
- "UsageLimitExceeded" in str(type(e).__name__)
534
- or "tool_calls_limit" in error_msg.lower()
535
- ):
536
- log.warning(
537
- "Global tool call quota reached - continuing with partial results"
538
- )
539
-
540
- result = ToolSelection(
541
- conversation=Conversation(
542
- status=ConversationStatus.COMPLETE,
543
- context="The agent reached the maximum number of tool calls allowed. Please try a more specific query or break down your request into smaller parts.",
544
- question=None,
545
- options=None,
546
- ),
547
- choices=[],
548
- explanation="Tool call limit reached during execution. Try refining your query.",
549
- reason=None,
550
- )
551
- else:
552
- raise
553
-
554
- # ---- 7) Convert raw tool call records into ToolRunLog objects ----------
555
- for tc in getattr(deps, "tool_calls", []):
556
- tool_name = tc.get("tool")
557
- timestamp = tc.get("timestamp")
558
- error = tc.get("error")
559
- inputs = {
560
- k: v for k, v in tc.items() if k not in ("tool", "timestamp", "error")
561
- }
562
- tool_logs.append(
563
- ToolRunLog(
564
- tool=tool_name,
565
- inputs=inputs,
566
- timestamp=timestamp,
567
- error=error,
568
- )
569
- )
570
-
571
- stage_counts: dict[str, int] = {}
572
- stage_durations: dict[str, float] = {}
573
- for tc in getattr(deps, "tool_calls", []):
574
- name = tc.get("tool", "unknown")
575
- stage_counts[name] = stage_counts.get(name, 0) + 1
576
- duration_ms = tc.get("duration_ms")
577
- if isinstance(duration_ms, (int, float)):
578
- stage_durations[name] = stage_durations.get(name, 0.0) + float(duration_ms)
579
-
580
- total_duration_ms = round((time.perf_counter() - run_started) * 1000, 1)
581
- log.info(
582
- "⏱️ Latency summary: total_ms=%s metadata_ms=%s llm_ms=%s tools=%s tool_ms=%s",
583
- total_duration_ms,
584
- metadata_duration_ms,
585
- llm_duration_ms,
586
- stage_counts,
587
- {k: round(v, 1) for k, v in stage_durations.items()},
588
- )
589
-
590
- # ---- 8) Extract usage statistics if available -------------------------
591
- usage_stats = None
592
- if run_result and hasattr(run_result, "usage") and run_result.usage:
593
- usage = run_result.usage()
594
- usage_stats = UsageStats(
595
- total_tokens=usage.total_tokens,
596
- input_tokens=usage.input_tokens,
597
- output_tokens=usage.output_tokens,
598
- )
599
-
600
- # ---- 9) Wrap into high-level AgentToolSelection ------------------------
601
- return AgentToolSelection(
602
- conversation=result.conversation,
603
- choices=result.choices,
604
- explanation=result.explanation,
605
- reason=result.reason,
606
- tool_calls=tool_logs,
607
- usage=usage_stats,
608
- )
609
-
610
-
611
- __all__ = ["run_agent", "agent"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import logging
5
+ import time
6
+ import asyncio
7
+ import threading
8
+ from collections import OrderedDict
9
+ from datetime import datetime
10
+ from typing import List
11
+
12
+ from pydantic_ai import Agent, RunContext
13
+ from pydantic_ai.usage import UsageLimits
14
+ from pydantic_ai.models.openai import OpenAIResponsesModel, OpenAIChatModel
15
+ from pydantic_ai.providers.openai import OpenAIProvider
16
+ from pydantic_ai.messages import BinaryContent
17
+
18
+ from ai_agent.generator.prompts import get_agent_system_prompt
19
+ from ai_agent.generator.schema import ToolSelection, Conversation, ConversationStatus
20
+ from ai_agent.utils.config import get_config
21
+ from .models import AgentToolSelection, ToolRunLog, UsageStats
22
+ from .tools.repo_info_tool import tool_repo_summary, RepoSummaryInput
23
+ from ai_agent.agent.utils import coerce_github_url_or_none
24
+ from .tools.search_tool import tool_search_tools, SearchToolsInput
25
+ from .tools.search_alternative_tool import (
26
+ tool_search_alternative,
27
+ SearchAlternativeInput,
28
+ )
29
+ from .tools.sparql_tool import tool_sparql_query, SparqlQueryInput
30
+ from .tools.query_utils import sanitize_retrieval_query
31
+ from .utils import AgentState, limit_tool_calls, cap_prepare
32
+ from ai_agent.utils.image_meta import summarize_image_metadata, detect_ext_token
33
+
34
+ log = logging.getLogger("agent.core")
35
+
36
+ DEFAULT_NUM_CHOICES = int(os.getenv("NUM_CHOICES", "3"))
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Dynamic agent instance cache
40
+ # Key: (model_name, base_url, api_key_env, num_choices)
41
+ # Avoids rebuilding Agent/OpenAIProvider/model objects on every request when
42
+ # the UI repeatedly uses the same custom endpoint + model combination.
43
+ # Bounded LRU (max AGENT_CACHE_MAX entries); protected by a lock so that
44
+ # concurrent requests cannot race while creating/inserting agents.
45
+ # ---------------------------------------------------------------------------
46
+ _AGENT_CACHE_MAX: int = int(os.getenv("AGENT_CACHE_MAX", "16"))
47
+ _AGENT_CACHE_LOCK: threading.Lock = threading.Lock()
48
+ _AGENT_CACHE: OrderedDict[tuple, "Agent"] = OrderedDict()
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Model / provider setup
52
+ # ---------------------------------------------------------------------------
53
+ config = get_config()
54
+ agent_model_config = config.agent_model
55
+
56
+ try:
57
+ api_key = agent_model_config.get_api_key()
58
+ except ValueError as e:
59
+ log.error(f"Failed to get API key for agent model: {e}")
60
+ raise
61
+
62
+ log.info(f"Initializing agent model: {agent_model_config.name}")
63
+
64
+ if agent_model_config.base_url:
65
+ log.info(f"Using custom OpenAI base URL: {agent_model_config.base_url}")
66
+ log.info("Using OpenAIChatModel (chat/completions API) for custom endpoint")
67
+ provider = OpenAIProvider(
68
+ base_url=agent_model_config.base_url,
69
+ api_key=api_key,
70
+ )
71
+ openai_model = OpenAIChatModel(
72
+ model_name=agent_model_config.name,
73
+ provider=provider,
74
+ )
75
+ else:
76
+ provider = OpenAIProvider(api_key=api_key)
77
+ openai_model = OpenAIResponsesModel(
78
+ model_name=agent_model_config.name,
79
+ provider=provider,
80
+ )
81
+
82
+ # ---------------------------------------------------------------------------
83
+ # Agent definition
84
+ # ---------------------------------------------------------------------------
85
+ agent = Agent(
86
+ model=openai_model,
87
+ system_prompt=get_agent_system_prompt(DEFAULT_NUM_CHOICES),
88
+ deps_type=AgentState,
89
+ output_retries=int(os.getenv("AGENT_OUTPUT_RETRIES", "3")),
90
+ )
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Tool adapters for the agent
94
+ # ---------------------------------------------------------------------------
95
+
96
+
97
+ @agent.tool(retries=2, prepare=cap_prepare)
98
+ @limit_tool_calls("search_tools", cap=1)
99
+ async def search_tools(
100
+ ctx: RunContext[AgentState],
101
+ query: str,
102
+ excluded: List[str] | None = None,
103
+ top_k: int = 12,
104
+ ) -> List[dict]:
105
+ """
106
+ Agent-facing search tool.
107
+
108
+ Delegates to tools.search_tool.tool_search_tools(), but automatically
109
+ injects:
110
+ - globally excluded tools (from ctx.deps.excluded_tools)
111
+ - image_paths and original_formats (from ctx.deps, set in run_agent)
112
+ so the language model never has to reason about file paths directly.
113
+ """
114
+ # Merge explicit exclusions with global exclusions from AgentState
115
+ explicit_excluded = excluded or []
116
+ global_excluded = getattr(ctx.deps, "excluded_tools", []) or []
117
+ all_excluded = sorted(set(explicit_excluded + list(global_excluded)))
118
+
119
+ original_formats = getattr(ctx.deps, "original_formats", []) or []
120
+ image_paths = getattr(ctx.deps, "image_paths", []) or []
121
+
122
+ effective_top_k = (
123
+ ctx.deps.override_top_k if ctx.deps.override_top_k is not None else top_k
124
+ )
125
+
126
+ started = time.perf_counter()
127
+ inp = SearchToolsInput(
128
+ query=sanitize_retrieval_query(query),
129
+ excluded=all_excluded,
130
+ top_k=effective_top_k,
131
+ original_formats=original_formats,
132
+ image_paths=image_paths,
133
+ )
134
+ out = tool_search_tools(inp)
135
+
136
+ ctx.deps.tool_calls.append(
137
+ {
138
+ "tool": "search_tools",
139
+ "query": query,
140
+ "count": len(out.candidates),
141
+ "duration_ms": round((time.perf_counter() - started) * 1000, 1),
142
+ "original_formats": original_formats,
143
+ "excluded": all_excluded,
144
+ "timestamp": datetime.now().isoformat(),
145
+ }
146
+ )
147
+
148
+ # Return plain dicts so the LLM sees a simple JSON-like structure.
149
+ return [c.model_dump(mode="python") for c in out.candidates]
150
+
151
+
152
+ @agent.tool(retries=2, prepare=cap_prepare)
153
+ @limit_tool_calls("search_alternative", cap=3)
154
+ async def search_alternative(
155
+ ctx: RunContext[AgentState],
156
+ alternative_query: str,
157
+ excluded: List[str] | None = None,
158
+ top_k: int = 12,
159
+ ) -> List[dict]:
160
+ """
161
+ Search with an alternative query formulation (includes automatic reranking).
162
+ """
163
+ explicit_excluded = excluded or []
164
+ global_excluded = getattr(ctx.deps, "excluded_tools", []) or []
165
+ all_excluded = sorted(set(explicit_excluded + list(global_excluded)))
166
+
167
+ original_formats = getattr(ctx.deps, "original_formats", []) or []
168
+ image_paths = getattr(ctx.deps, "image_paths", []) or []
169
+
170
+ started = time.perf_counter()
171
+ inp = SearchAlternativeInput(
172
+ alternative_query=alternative_query,
173
+ excluded=all_excluded,
174
+ top_k=top_k,
175
+ original_formats=original_formats,
176
+ image_paths=image_paths,
177
+ )
178
+ out = tool_search_alternative(inp)
179
+
180
+ ctx.deps.tool_calls.append(
181
+ {
182
+ "tool": "search_alternative",
183
+ "alternative_query": alternative_query,
184
+ "query_used": out.query_used,
185
+ "count": len(out.candidates),
186
+ "duration_ms": round((time.perf_counter() - started) * 1000, 1),
187
+ "original_formats": original_formats,
188
+ "excluded": all_excluded,
189
+ "timestamp": datetime.now().isoformat(),
190
+ }
191
+ )
192
+
193
+ return [c.model_dump(mode="python") for c in out.candidates]
194
+
195
+
196
+ @agent.tool(retries=1, prepare=cap_prepare)
197
+ @limit_tool_calls("sparql_query", cap=3)
198
+ async def sparql_query(
199
+ ctx: RunContext[AgentState],
200
+ query: str,
201
+ limit: int = 50,
202
+ ) -> dict:
203
+ """Run a read-only SPARQL SELECT / ASK against the GraphDB catalog.
204
+
205
+ Use this for questions the semantic search can't answer cleanly —
206
+ aggregates ("how many tools support DICOM?"), structural filters
207
+ ("tools that require a GPU AND are free"), distinct property values
208
+ ("which licences appear in the catalog?"), etc.
209
+
210
+ UPDATE-style operations are rejected. Results capped at `limit` rows
211
+ (max 200). Returns columns + rows, plus a boolean for ASK queries.
212
+ """
213
+ started = time.perf_counter()
214
+ inp = SparqlQueryInput(query=query, limit=max(1, min(200, int(limit))))
215
+ out = tool_sparql_query(inp)
216
+ ctx.deps.tool_calls.append(
217
+ {
218
+ "tool": "sparql_query",
219
+ "query": query[:300],
220
+ "row_count": out.row_count,
221
+ "truncated": out.truncated,
222
+ "boolean": out.boolean,
223
+ "error": out.error,
224
+ "duration_ms": round((time.perf_counter() - started) * 1000, 1),
225
+ "timestamp": datetime.now().isoformat(),
226
+ }
227
+ )
228
+ return out.model_dump(mode="python")
229
+
230
+
231
+ @agent.tool(retries=2, prepare=cap_prepare)
232
+ @limit_tool_calls("repo_info_batch", cap=4)
233
+ async def repo_info_batch(
234
+ ctx: RunContext[AgentState],
235
+ urls: List[str],
236
+ ) -> List[dict]:
237
+ """Fetch repository summaries for multiple repositories in parallel."""
238
+ started = time.perf_counter()
239
+
240
+ if not urls:
241
+ return []
242
+
243
+ normalized: List[str] = []
244
+ skipped: List[dict] = []
245
+ seen: set[str] = set()
246
+ for raw in urls:
247
+ norm = coerce_github_url_or_none(raw)
248
+ if not norm:
249
+ skipped.append(
250
+ {
251
+ "url": raw,
252
+ "skipped": True,
253
+ "reason": "NON_GITHUB_URL",
254
+ }
255
+ )
256
+ continue
257
+ if norm in seen:
258
+ continue
259
+ seen.add(norm)
260
+ normalized.append(norm)
261
+
262
+ tasks = [tool_repo_summary(RepoSummaryInput(url=u)) for u in normalized]
263
+ outcomes = await asyncio.gather(*tasks, return_exceptions=True)
264
+
265
+ results: List[dict] = []
266
+ for url, outcome in zip(normalized, outcomes):
267
+ if isinstance(outcome, Exception):
268
+ results.append(
269
+ {
270
+ "url": url,
271
+ "source": "error",
272
+ "error": str(outcome),
273
+ }
274
+ )
275
+ continue
276
+ payload = outcome.model_dump(mode="python")
277
+ payload["url"] = url
278
+ results.append(payload)
279
+
280
+ if skipped:
281
+ results.extend(skipped)
282
+
283
+ ctx.deps.tool_calls.append(
284
+ {
285
+ "tool": "repo_info_batch",
286
+ "requested": len(urls),
287
+ "normalized": len(normalized),
288
+ "returned": len(results),
289
+ "duration_ms": round((time.perf_counter() - started) * 1000, 1),
290
+ "timestamp": datetime.now().isoformat(),
291
+ }
292
+ )
293
+
294
+ return results
295
+
296
+
297
+ # ---------------------------------------------------------------------------
298
+ # High level entry point: run the agent on (text query + image)
299
+ # ---------------------------------------------------------------------------
300
+ def run_agent(
301
+ task: str,
302
+ image_paths: List[str],
303
+ excluded: List[str] | None = None,
304
+ conversation_history: List[str] | None = None,
305
+ *,
306
+ image_bytes: bytes | None = None,
307
+ model: str | None = None,
308
+ base_url: str | None = None,
309
+ api_key_env: str | None = None,
310
+ top_k: int | None = None,
311
+ num_choices: int | None = None,
312
+ image_metadata: str | None = None,
313
+ ) -> AgentToolSelection:
314
+ """
315
+ Execute the agent for a user task and at least one image path.
316
+
317
+ - derive canonical original_formats (tiff / dicom / nifti / ...)
318
+ - build a compact image metadata summary (or use pre-computed one)
319
+ - pass both to the LLM as hidden context
320
+ - store image_paths/original_formats in deps so retrieval tools can use them
321
+ - optionally allow runtime model/base_url/top_k/num_choices overrides
322
+
323
+ IMPORTANT:
324
+ The model only sees an actual image if `image_bytes` is provided.
325
+ `image_paths` are used for metadata + tool context only.
326
+ """
327
+ run_started = time.perf_counter()
328
+ # image_paths may now be empty — the agent runs in text-only mode and
329
+ # skips the VLM. Retrieval still works on the text query alone.
330
+ image_paths = list(image_paths or [])
331
+
332
+ tool_logs: List[ToolRunLog] = []
333
+
334
+ # ---- 1) Derive image-based metadata and format hints --------------------
335
+ metadata_started = time.perf_counter()
336
+ meta_str = (
337
+ image_metadata
338
+ if image_metadata is not None
339
+ else (summarize_image_metadata(image_paths) or "")
340
+ )
341
+ fmt_str = detect_ext_token(image_paths) or ""
342
+ original_formats = [t.lower() for t in fmt_str.split()] if fmt_str else []
343
+ metadata_duration_ms = round((time.perf_counter() - metadata_started) * 1000, 1)
344
+
345
+ effective_top_k = top_k if top_k is not None else 12
346
+ effective_num_choices = num_choices if num_choices is not None else 3
347
+
348
+ # ---- 2) Prepare dependency state passed to all tools --------------------
349
+ deps = AgentState(
350
+ excluded_tools=excluded or [],
351
+ override_model=model,
352
+ override_base_url=base_url,
353
+ override_top_k=effective_top_k,
354
+ override_num_choices=effective_num_choices,
355
+ )
356
+
357
+ setattr(deps, "image_paths", list(image_paths))
358
+ setattr(deps, "original_formats", original_formats)
359
+
360
+ # ---- 3) Hidden metadata lines for the model ----------------------------
361
+ hidden_meta = ""
362
+ if original_formats:
363
+ hidden_meta += "\n(Formats Hint: " + ",".join(original_formats) + ")"
364
+ if meta_str:
365
+ short_meta = " ".join(x.strip() for x in meta_str.splitlines() if x.strip())
366
+ hidden_meta += (
367
+ "\n(Image Metadata: "
368
+ + short_meta[:500]
369
+ + ("…" if len(short_meta) > 500 else "")
370
+ + ")"
371
+ )
372
+ hidden_meta += f"\n(Search top_k: {effective_top_k})"
373
+
374
+ extra_context = "\n\n**CRITICAL: Analyze the attached preview image showing the user's data.**\nUse visual observations (anatomy visible, image quality, dimensionality, contrast) combined with the metadata below to recommend tools. Reference what you see in your explanations."
375
+
376
+ # ---- 4) Build the prompt (optionally including history) ----------------
377
+ if conversation_history and len(conversation_history) > 0:
378
+ history_text = "\n".join(conversation_history)
379
+ prompt = (
380
+ f"Previous conversation:\n{history_text}\n\n"
381
+ f"Current request: {task}{extra_context}{hidden_meta}"
382
+ )
383
+ else:
384
+ prompt = task + extra_context + hidden_meta
385
+
386
+ # -----------------------------------------------------------------------
387
+ # Determine which agent instance to use
388
+ # -----------------------------------------------------------------------
389
+ agent_instance = agent
390
+ effective_num_choices = num_choices if num_choices is not None else 3
391
+ effective_model = model if model else agent_model_config.name
392
+ effective_top_k = top_k if top_k is not None else 12
393
+
394
+ # When model is provided from UI, base_url comes with it (can be None for OpenAI)
395
+ if model:
396
+ # Use api_key_env from config if provided, otherwise default to OPENAI_API_KEY
397
+ key_env_name = api_key_env if api_key_env else "OPENAI_API_KEY"
398
+ runtime_api_key = os.getenv(key_env_name)
399
+ if not runtime_api_key:
400
+ raise ValueError(
401
+ f"{key_env_name} not found in environment. Cannot use this model."
402
+ )
403
+ effective_base_url = base_url # Can be None for OpenAI
404
+ log.info(f"✓ Using {key_env_name} for model {effective_model}")
405
+ log.debug(f"{key_env_name} is set: {bool(runtime_api_key)}")
406
+ else:
407
+ # No model override - use config defaults
408
+ effective_base_url = agent_model_config.base_url
409
+ runtime_api_key = api_key # Already loaded from config at startup
410
+ log.info(f"✓ Using API key from config for model {effective_model}")
411
+
412
+ # Log runtime configuration
413
+ endpoint_display = effective_base_url if effective_base_url else "api.openai.com"
414
+ log.info(
415
+ f"🤖 Agent execution - Model: {effective_model}, endpoint: {endpoint_display}, "
416
+ f"top_k: {effective_top_k}, num_choices: {effective_num_choices}, excluded: {len(excluded or [])}"
417
+ )
418
+
419
+ needs_dynamic_agent = model is not None
420
+
421
+ if needs_dynamic_agent:
422
+ cache_key = (effective_model, effective_base_url or "", api_key_env or "OPENAI_API_KEY", effective_num_choices)
423
+ with _AGENT_CACHE_LOCK:
424
+ agent_instance = _AGENT_CACHE.get(cache_key)
425
+ if agent_instance is not None:
426
+ _AGENT_CACHE.move_to_end(cache_key)
427
+ if agent_instance is None:
428
+ log.info(
429
+ f"📦 Creating runtime agent with model={effective_model}, endpoint={effective_base_url or 'api.openai.com'}"
430
+ )
431
+
432
+ runtime_provider = OpenAIProvider(
433
+ base_url=effective_base_url,
434
+ api_key=runtime_api_key,
435
+ )
436
+
437
+ # Use OpenAIChatModel (chat/completions) for custom endpoints, OpenAIResponsesModel for default OpenAI
438
+ if effective_base_url:
439
+ log.info("Using OpenAIChatModel (chat/completions API) for custom endpoint")
440
+ runtime_model = OpenAIChatModel(
441
+ model_name=effective_model, provider=runtime_provider
442
+ )
443
+ else:
444
+ runtime_model = OpenAIResponsesModel(
445
+ model_name=effective_model, provider=runtime_provider
446
+ )
447
+
448
+ agent_instance = Agent(
449
+ model=runtime_model,
450
+ system_prompt=get_agent_system_prompt(effective_num_choices),
451
+ deps_type=AgentState,
452
+ output_retries=int(os.getenv("AGENT_OUTPUT_RETRIES", "3")),
453
+ )
454
+
455
+ # Register tools on the dynamic agent
456
+ agent_instance.tool(search_tools, retries=2, prepare=cap_prepare)
457
+ agent_instance.tool(search_alternative, retries=2, prepare=cap_prepare)
458
+ agent_instance.tool(repo_info_batch, retries=2, prepare=cap_prepare)
459
+ agent_instance.tool(sparql_query, retries=1, prepare=cap_prepare)
460
+
461
+ with _AGENT_CACHE_LOCK:
462
+ _AGENT_CACHE[cache_key] = agent_instance
463
+ _AGENT_CACHE.move_to_end(cache_key)
464
+ while len(_AGENT_CACHE) > _AGENT_CACHE_MAX:
465
+ _AGENT_CACHE.popitem(last=False)
466
+ else:
467
+ log.info(
468
+ f"♻️ Reusing cached dynamic agent (model: {effective_model}, num_choices: {effective_num_choices})"
469
+ )
470
+
471
+ elif (
472
+ num_choices is not None and num_choices != DEFAULT_NUM_CHOICES
473
+ ):
474
+ cache_key = (effective_model, effective_base_url or "", api_key_env or "OPENAI_API_KEY", effective_num_choices)
475
+ with _AGENT_CACHE_LOCK:
476
+ agent_instance = _AGENT_CACHE.get(cache_key)
477
+ if agent_instance is not None:
478
+ _AGENT_CACHE.move_to_end(cache_key)
479
+ if agent_instance is None:
480
+ log.info(
481
+ f"📦 Creating runtime agent with num_choices={effective_num_choices} (model: {effective_model})"
482
+ )
483
+ agent_instance = Agent(
484
+ model=openai_model,
485
+ system_prompt=get_agent_system_prompt(effective_num_choices),
486
+ deps_type=AgentState,
487
+ output_retries=int(os.getenv("AGENT_OUTPUT_RETRIES", "3")),
488
+ )
489
+
490
+ # Register tools on the dynamic agent
491
+ agent_instance.tool(search_tools, retries=2, prepare=cap_prepare)
492
+ agent_instance.tool(search_alternative, retries=2, prepare=cap_prepare)
493
+ agent_instance.tool(repo_info_batch, retries=2, prepare=cap_prepare)
494
+ agent_instance.tool(sparql_query, retries=1, prepare=cap_prepare)
495
+
496
+ with _AGENT_CACHE_LOCK:
497
+ _AGENT_CACHE[cache_key] = agent_instance
498
+ _AGENT_CACHE.move_to_end(cache_key)
499
+ while len(_AGENT_CACHE) > _AGENT_CACHE_MAX:
500
+ _AGENT_CACHE.popitem(last=False)
501
+ else:
502
+ log.info(
503
+ f"♻️ Reusing cached dynamic agent with num_choices={effective_num_choices} (model: {effective_model})"
504
+ )
505
+
506
+ else:
507
+ log.info(
508
+ f"♻️ Using global agent (model: {effective_model}, num_choices: {effective_num_choices})"
509
+ )
510
+
511
+ log.debug(
512
+ f"Prompt length: {len(prompt)} chars, has_image_paths: {bool(image_paths)}, has_image_bytes: {bool(image_bytes)}"
513
+ )
514
+
515
+ # ---- 5) Build multimodal prompt if image bytes provided ----------------
516
+ if image_bytes:
517
+ log.info(
518
+ f"🖼️ Sending image preview to model ({len(image_bytes)} bytes = {len(image_bytes)/1024:.1f}KB)"
519
+ )
520
+ user_prompt = [
521
+ prompt,
522
+ BinaryContent(
523
+ data=image_bytes,
524
+ media_type="image/png",
525
+ ),
526
+ ]
527
+ else:
528
+ log.warning(
529
+ "⚠️ No image bytes provided - the model will not see the image preview"
530
+ )
531
+ user_prompt = prompt
532
+
533
+ # ---- 6) Run the agent --------------------------------------------------
534
+ try:
535
+ llm_started = time.perf_counter()
536
+ run_result = agent_instance.run_sync(
537
+ user_prompt,
538
+ deps=deps,
539
+ output_type=ToolSelection,
540
+ usage_limits=UsageLimits(tool_calls_limit=20),
541
+ )
542
+ llm_duration_ms = round((time.perf_counter() - llm_started) * 1000, 1)
543
+ result = run_result.output
544
+
545
+ log.info(
546
+ f"✅ Agent execution complete - choices returned: {len(result.choices)}"
547
+ )
548
+
549
+ # Log usage (helpful, but may not explicitly expose image-specific counters)
550
+ if run_result.usage:
551
+ usage = run_result.usage()
552
+ log.info(
553
+ f"📊 Usage: total_tokens={usage.total_tokens}, "
554
+ f"input_tokens={usage.input_tokens}, output_tokens={usage.output_tokens}"
555
+ )
556
+
557
+ # Warn if using non-OpenAI endpoint with images
558
+ if image_bytes and effective_base_url:
559
+ log.warning(
560
+ "⚠️ Using custom endpoint - confirm the selected model supports vision."
561
+ )
562
+
563
+ except Exception as e:
564
+ # Handle global tool quota limit (UsageLimitExceeded) and other errors gracefully
565
+ error_msg = str(e)
566
+ llm_duration_ms = round((time.perf_counter() - llm_started) * 1000, 1)
567
+ log.warning(f"⚠️ Agent execution encountered an error: {error_msg}")
568
+ run_result = None # Ensure run_result is defined for usage stats extraction
569
+
570
+ # Check if this is a usage limit error (global tool quota)
571
+ if (
572
+ "UsageLimitExceeded" in str(type(e).__name__)
573
+ or "tool_calls_limit" in error_msg.lower()
574
+ ):
575
+ log.warning(
576
+ "Global tool call quota reached - continuing with partial results"
577
+ )
578
+
579
+ result = ToolSelection(
580
+ conversation=Conversation(
581
+ status=ConversationStatus.COMPLETE,
582
+ context="The agent reached the maximum number of tool calls allowed. Please try a more specific query or break down your request into smaller parts.",
583
+ question=None,
584
+ options=None,
585
+ ),
586
+ choices=[],
587
+ explanation="Tool call limit reached during execution. Try refining your query.",
588
+ reason=None,
589
+ )
590
+ else:
591
+ raise
592
+
593
+ # ---- 7) Convert raw tool call records into ToolRunLog objects ----------
594
+ for tc in getattr(deps, "tool_calls", []):
595
+ tool_name = tc.get("tool")
596
+ timestamp = tc.get("timestamp")
597
+ error = tc.get("error")
598
+ inputs = {
599
+ k: v for k, v in tc.items() if k not in ("tool", "timestamp", "error")
600
+ }
601
+ tool_logs.append(
602
+ ToolRunLog(
603
+ tool=tool_name,
604
+ inputs=inputs,
605
+ timestamp=timestamp,
606
+ error=error,
607
+ )
608
+ )
609
+
610
+ stage_counts: dict[str, int] = {}
611
+ stage_durations: dict[str, float] = {}
612
+ for tc in getattr(deps, "tool_calls", []):
613
+ name = tc.get("tool", "unknown")
614
+ stage_counts[name] = stage_counts.get(name, 0) + 1
615
+ duration_ms = tc.get("duration_ms")
616
+ if isinstance(duration_ms, (int, float)):
617
+ stage_durations[name] = stage_durations.get(name, 0.0) + float(duration_ms)
618
+
619
+ total_duration_ms = round((time.perf_counter() - run_started) * 1000, 1)
620
+ log.info(
621
+ "⏱️ Latency summary: total_ms=%s metadata_ms=%s llm_ms=%s tools=%s tool_ms=%s",
622
+ total_duration_ms,
623
+ metadata_duration_ms,
624
+ llm_duration_ms,
625
+ stage_counts,
626
+ {k: round(v, 1) for k, v in stage_durations.items()},
627
+ )
628
+
629
+ # ---- 8) Extract usage statistics if available -------------------------
630
+ usage_stats = None
631
+ if run_result and hasattr(run_result, "usage") and run_result.usage:
632
+ usage = run_result.usage()
633
+ usage_stats = UsageStats(
634
+ total_tokens=usage.total_tokens,
635
+ input_tokens=usage.input_tokens,
636
+ output_tokens=usage.output_tokens,
637
+ )
638
+
639
+ # ---- 9) Wrap into high-level AgentToolSelection ------------------------
640
+ return AgentToolSelection(
641
+ conversation=result.conversation,
642
+ choices=result.choices,
643
+ explanation=result.explanation,
644
+ reason=result.reason,
645
+ tool_calls=tool_logs,
646
+ usage=usage_stats,
647
+ )
648
+
649
+
650
+ __all__ = ["run_agent", "agent"]
src/ai_agent/agent/tools/__init__.py CHANGED
@@ -1,38 +1,38 @@
1
- """Agent tools package."""
2
-
3
- # Only export registry - tools will self-register when imported explicitly
4
- from .mcp import (
5
- TOOL_REGISTRY,
6
- get_tool,
7
- register_tool,
8
- list_tools,
9
- ensure_mcp_tools_registered,
10
- )
11
-
12
- # Import tools lazily to avoid loading heavy dependencies at package import
13
- # Tools should be imported explicitly where needed, e.g.:
14
- # from ai_agent.agent.tools.mcp.lungs_segmentation_tool import tool_lungs_segmentation
15
-
16
- __all__ = [
17
- "TOOL_REGISTRY",
18
- "get_tool",
19
- "register_tool",
20
- "list_tools",
21
- "ensure_tools_registered",
22
- ]
23
-
24
-
25
- def ensure_tools_registered():
26
- """
27
- Import all tools to trigger their registration.
28
- Call this once at app startup.
29
- """
30
- from importlib import import_module
31
-
32
- import_module("ai_agent.agent.tools.search_tool")
33
- import_module("ai_agent.agent.tools.search_alternative_tool")
34
- import_module("ai_agent.agent.tools.repo_info_tool")
35
- import_module("ai_agent.agent.tools.gradio_space_tool")
36
-
37
- # Import MCP tools
38
- ensure_mcp_tools_registered()
 
1
+ """Agent tools package."""
2
+
3
+ # Only export registry - tools will self-register when imported explicitly
4
+ from .mcp import (
5
+ TOOL_REGISTRY,
6
+ get_tool,
7
+ register_tool,
8
+ list_tools,
9
+ ensure_mcp_tools_registered,
10
+ )
11
+
12
+ # Import tools lazily to avoid loading heavy dependencies at package import
13
+ # Tools should be imported explicitly where needed, e.g.:
14
+ # from ai_agent.agent.tools.mcp.lungs_segmentation_tool import tool_lungs_segmentation
15
+
16
+ __all__ = [
17
+ "TOOL_REGISTRY",
18
+ "get_tool",
19
+ "register_tool",
20
+ "list_tools",
21
+ "ensure_tools_registered",
22
+ ]
23
+
24
+
25
+ def ensure_tools_registered():
26
+ """
27
+ Import all tools to trigger their registration.
28
+ Call this once at app startup.
29
+ """
30
+ from importlib import import_module
31
+
32
+ import_module("ai_agent.agent.tools.search_tool")
33
+ import_module("ai_agent.agent.tools.search_alternative_tool")
34
+ import_module("ai_agent.agent.tools.repo_info_tool")
35
+ import_module("ai_agent.agent.tools.gradio_space_tool")
36
+
37
+ # Import MCP tools
38
+ ensure_mcp_tools_registered()
src/ai_agent/agent/tools/deepwiki_tool.py CHANGED
@@ -1,103 +1,103 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- import logging
5
- import os
6
- from typing import Optional
7
-
8
- from pydantic import BaseModel
9
- from pydantic_ai.mcp import MCPServerStreamableHTTP
10
-
11
- from ai_agent.agent.tools.utils import _clip
12
- from ai_agent.agent.utils import _coerce_owner_repo_ref
13
-
14
- log = logging.getLogger("agent.deepwiki")
15
-
16
- # DeepWiki MCP server endpoint (Streamable HTTP transport)
17
- DEEPWIKI_HTTP_URL = "https://mcp.deepwiki.com/mcp"
18
-
19
- # Timeout for DeepWiki operations (seconds)
20
- DEEPWIKI_TIMEOUT = int(os.getenv("DEEPWIKI_TIMEOUT", "20"))
21
-
22
-
23
- class DeepWikiInput(BaseModel):
24
- """Input for DeepWiki operations."""
25
-
26
- url: str # GitHub repository URL or owner/repo format
27
-
28
-
29
- class DeepWikiContentsOutput(BaseModel):
30
- """Output from read_wiki_contents."""
31
-
32
- success: bool
33
- contents: Optional[str] = None
34
- error: Optional[str] = None
35
- truncated: bool = False
36
-
37
-
38
- async def get_wiki_contents(input: DeepWikiInput) -> DeepWikiContentsOutput:
39
- """
40
- Fetch repo docs from DeepWiki MCP (Streamable HTTP) and return a clipped string
41
- to keep LLM token usage under control.
42
- """
43
- owner, repo, _ = _coerce_owner_repo_ref(input.url)
44
- repo = f"{owner}/{repo}"
45
-
46
- try:
47
- server = MCPServerStreamableHTTP(DEEPWIKI_HTTP_URL)
48
-
49
- async with server:
50
- result = await asyncio.wait_for(
51
- server.direct_call_tool("read_wiki_contents", {"repoName": repo}),
52
- timeout=DEEPWIKI_TIMEOUT,
53
- )
54
-
55
- # Handle different result types from MCP
56
- text = None
57
- if isinstance(result, str):
58
- # Direct string result
59
- text = result
60
- elif hasattr(result, "content"):
61
- # MCP ToolResult with content field
62
- text_parts = []
63
- for item in result.content:
64
- if hasattr(item, "text"):
65
- text_parts.append(item.text)
66
- elif isinstance(item, str):
67
- text_parts.append(item)
68
- text = "\n".join(text_parts) if text_parts else None
69
- elif isinstance(result, list):
70
- # List of strings or content items
71
- text = "\n".join([str(p) for p in result if p]) or None
72
-
73
- if text and text.strip():
74
- clipped_text, truncated = _clip(text.strip())
75
- return DeepWikiContentsOutput(
76
- success=True,
77
- contents=clipped_text,
78
- truncated=truncated,
79
- )
80
-
81
- return DeepWikiContentsOutput(
82
- success=False, error="No content returned from DeepWiki"
83
- )
84
-
85
- except asyncio.TimeoutError:
86
- log.warning(f"DeepWiki timed out after {DEEPWIKI_TIMEOUT}s for {repo}")
87
- return DeepWikiContentsOutput(
88
- success=False,
89
- error=f"DeepWiki request timed out after {DEEPWIKI_TIMEOUT}s",
90
- )
91
- except Exception as e:
92
- log.error(f"Failed to get wiki contents for {repo}: {e}")
93
- return DeepWikiContentsOutput(
94
- success=False,
95
- error=f"Failed to connect to DeepWiki: {str(e)}",
96
- )
97
-
98
-
99
- __all__ = [
100
- "get_wiki_contents",
101
- "DeepWikiInput",
102
- "DeepWikiContentsOutput",
103
- ]
 
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import os
6
+ from typing import Optional
7
+
8
+ from pydantic import BaseModel
9
+ from pydantic_ai.mcp import MCPServerStreamableHTTP
10
+
11
+ from ai_agent.agent.tools.utils import _clip
12
+ from ai_agent.agent.utils import _coerce_owner_repo_ref
13
+
14
+ log = logging.getLogger("agent.deepwiki")
15
+
16
+ # DeepWiki MCP server endpoint (Streamable HTTP transport)
17
+ DEEPWIKI_HTTP_URL = "https://mcp.deepwiki.com/mcp"
18
+
19
+ # Timeout for DeepWiki operations (seconds)
20
+ DEEPWIKI_TIMEOUT = int(os.getenv("DEEPWIKI_TIMEOUT", "20"))
21
+
22
+
23
+ class DeepWikiInput(BaseModel):
24
+ """Input for DeepWiki operations."""
25
+
26
+ url: str # GitHub repository URL or owner/repo format
27
+
28
+
29
+ class DeepWikiContentsOutput(BaseModel):
30
+ """Output from read_wiki_contents."""
31
+
32
+ success: bool
33
+ contents: Optional[str] = None
34
+ error: Optional[str] = None
35
+ truncated: bool = False
36
+
37
+
38
+ async def get_wiki_contents(input: DeepWikiInput) -> DeepWikiContentsOutput:
39
+ """
40
+ Fetch repo docs from DeepWiki MCP (Streamable HTTP) and return a clipped string
41
+ to keep LLM token usage under control.
42
+ """
43
+ owner, repo, _ = _coerce_owner_repo_ref(input.url)
44
+ repo = f"{owner}/{repo}"
45
+
46
+ try:
47
+ server = MCPServerStreamableHTTP(DEEPWIKI_HTTP_URL)
48
+
49
+ async with server:
50
+ result = await asyncio.wait_for(
51
+ server.direct_call_tool("read_wiki_contents", {"repoName": repo}),
52
+ timeout=DEEPWIKI_TIMEOUT,
53
+ )
54
+
55
+ # Handle different result types from MCP
56
+ text = None
57
+ if isinstance(result, str):
58
+ # Direct string result
59
+ text = result
60
+ elif hasattr(result, "content"):
61
+ # MCP ToolResult with content field
62
+ text_parts = []
63
+ for item in result.content:
64
+ if hasattr(item, "text"):
65
+ text_parts.append(item.text)
66
+ elif isinstance(item, str):
67
+ text_parts.append(item)
68
+ text = "\n".join(text_parts) if text_parts else None
69
+ elif isinstance(result, list):
70
+ # List of strings or content items
71
+ text = "\n".join([str(p) for p in result if p]) or None
72
+
73
+ if text and text.strip():
74
+ clipped_text, truncated = _clip(text.strip())
75
+ return DeepWikiContentsOutput(
76
+ success=True,
77
+ contents=clipped_text,
78
+ truncated=truncated,
79
+ )
80
+
81
+ return DeepWikiContentsOutput(
82
+ success=False, error="No content returned from DeepWiki"
83
+ )
84
+
85
+ except asyncio.TimeoutError:
86
+ log.warning(f"DeepWiki timed out after {DEEPWIKI_TIMEOUT}s for {repo}")
87
+ return DeepWikiContentsOutput(
88
+ success=False,
89
+ error=f"DeepWiki request timed out after {DEEPWIKI_TIMEOUT}s",
90
+ )
91
+ except Exception as e:
92
+ log.error(f"Failed to get wiki contents for {repo}: {e}")
93
+ return DeepWikiContentsOutput(
94
+ success=False,
95
+ error=f"Failed to connect to DeepWiki: {str(e)}",
96
+ )
97
+
98
+
99
+ __all__ = [
100
+ "get_wiki_contents",
101
+ "DeepWikiInput",
102
+ "DeepWikiContentsOutput",
103
+ ]
src/ai_agent/agent/tools/mcp/__init__.py CHANGED
@@ -1,53 +1,53 @@
1
- """
2
- MCP (Model Context Protocol) tools package.
3
-
4
- This package contains registered imaging tools that require approval
5
- and follow the tool registry pattern.
6
- """
7
-
8
- from .registry import (
9
- TOOL_REGISTRY,
10
- CATALOG_NAME_TO_TOOL,
11
- get_tool,
12
- register_tool,
13
- list_tools,
14
- get_tool_display_name,
15
- get_tool_icon,
16
- extract_preview,
17
- extract_downloads,
18
- extract_metadata,
19
- extract_output_field,
20
- ToolConfig,
21
- )
22
-
23
- from .base import BaseToolInput, BaseToolOutput, ImageToolInput
24
-
25
- __all__ = [
26
- # Registry
27
- "TOOL_REGISTRY",
28
- "CATALOG_NAME_TO_TOOL",
29
- "get_tool",
30
- "register_tool",
31
- "list_tools",
32
- "get_tool_display_name",
33
- "get_tool_icon",
34
- "extract_preview",
35
- "extract_downloads",
36
- "extract_metadata",
37
- "extract_output_field",
38
- "ToolConfig",
39
- # Base models
40
- "BaseToolInput",
41
- "BaseToolOutput",
42
- "ImageToolInput",
43
- ]
44
-
45
-
46
- def ensure_mcp_tools_registered():
47
- """
48
- Import all MCP tools to trigger their registration.
49
- Call this once at app startup.
50
- """
51
- from importlib import import_module
52
-
53
- import_module("ai_agent.agent.tools.mcp.lungs_segmentation_tool")
 
1
+ """
2
+ MCP (Model Context Protocol) tools package.
3
+
4
+ This package contains registered imaging tools that require approval
5
+ and follow the tool registry pattern.
6
+ """
7
+
8
+ from .registry import (
9
+ TOOL_REGISTRY,
10
+ CATALOG_NAME_TO_TOOL,
11
+ get_tool,
12
+ register_tool,
13
+ list_tools,
14
+ get_tool_display_name,
15
+ get_tool_icon,
16
+ extract_preview,
17
+ extract_downloads,
18
+ extract_metadata,
19
+ extract_output_field,
20
+ ToolConfig,
21
+ )
22
+
23
+ from .base import BaseToolInput, BaseToolOutput, ImageToolInput
24
+
25
+ __all__ = [
26
+ # Registry
27
+ "TOOL_REGISTRY",
28
+ "CATALOG_NAME_TO_TOOL",
29
+ "get_tool",
30
+ "register_tool",
31
+ "list_tools",
32
+ "get_tool_display_name",
33
+ "get_tool_icon",
34
+ "extract_preview",
35
+ "extract_downloads",
36
+ "extract_metadata",
37
+ "extract_output_field",
38
+ "ToolConfig",
39
+ # Base models
40
+ "BaseToolInput",
41
+ "BaseToolOutput",
42
+ "ImageToolInput",
43
+ ]
44
+
45
+
46
+ def ensure_mcp_tools_registered():
47
+ """
48
+ Import all MCP tools to trigger their registration.
49
+ Call this once at app startup.
50
+ """
51
+ from importlib import import_module
52
+
53
+ import_module("ai_agent.agent.tools.mcp.lungs_segmentation_tool")
src/ai_agent/agent/tools/mcp/base.py CHANGED
@@ -1,88 +1,88 @@
1
- from __future__ import annotations
2
-
3
- from typing import Optional, Dict, Any
4
- from pydantic import BaseModel, Field
5
-
6
-
7
- class BaseToolInput(BaseModel):
8
- """
9
- Base input model that tools can extend.
10
-
11
- Common patterns:
12
- - image_path: Path to uploaded image/volume
13
- - description: Optional context from agent
14
- """
15
-
16
- pass # Intentionally minimal - tools define their own inputs
17
-
18
-
19
- class BaseToolOutput(BaseModel):
20
- """
21
- Base output model that all tools should follow.
22
-
23
- This ensures consistent handling in the UI layer without
24
- needing tool-specific code.
25
-
26
- Standard fields:
27
- - success: bool - Whether execution succeeded
28
- - error: Optional[str] - Error message if failed
29
- - compute_time_seconds: float - Time taken by tool
30
- - notes: Optional[str] - Additional info for user
31
-
32
- File outputs (at least one should be provided on success):
33
- - result_preview: Optional[str] - PNG/GIF preview for inline display
34
- - result_origin: Optional[str] - Original format file for download
35
- - result_path: Optional[str] - Backward compat field
36
-
37
- Metadata:
38
- - metadata_text: Optional[str] - Structured info about result
39
- - metadata: Dict[str, Any] - Machine-readable metadata
40
-
41
- Tracking:
42
- - endpoint_url: str - API endpoint used
43
- - api_name: str - API method called
44
- """
45
-
46
- # Core status
47
- success: bool = False
48
- error: Optional[str] = None
49
- compute_time_seconds: float = 0.0
50
-
51
- # File outputs (tools should provide these for UI to display/download)
52
- result_preview: Optional[str] = Field(
53
- default=None,
54
- description="Path to preview image (PNG/GIF) for inline display in chat",
55
- )
56
- result_origin: Optional[str] = Field(
57
- default=None,
58
- description="Path to original format file (TIFF/NIfTI/DICOM) for download",
59
- )
60
- result_path: Optional[str] = Field(
61
- default=None, description="Backward compatibility: primary result path"
62
- )
63
-
64
- # Metadata
65
- metadata_text: Optional[str] = Field(
66
- default=None, description="Human-readable metadata about the result"
67
- )
68
- metadata: Dict[str, Any] = Field(
69
- default_factory=dict, description="Machine-readable metadata"
70
- )
71
- notes: Optional[str] = Field(
72
- default=None, description="Additional notes or context for user"
73
- )
74
-
75
- # Tracking/debugging
76
- endpoint_url: str = ""
77
- api_name: str = ""
78
-
79
-
80
- class ImageToolInput(BaseToolInput):
81
- """
82
- Common input pattern for image/volume processing tools.
83
- """
84
-
85
- image_path: str = Field(description="Path to the image/volume file")
86
- description: Optional[str] = Field(
87
- default=None, description="Optional context or notes from agent about the task"
88
- )
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, Dict, Any
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class BaseToolInput(BaseModel):
8
+ """
9
+ Base input model that tools can extend.
10
+
11
+ Common patterns:
12
+ - image_path: Path to uploaded image/volume
13
+ - description: Optional context from agent
14
+ """
15
+
16
+ pass # Intentionally minimal - tools define their own inputs
17
+
18
+
19
+ class BaseToolOutput(BaseModel):
20
+ """
21
+ Base output model that all tools should follow.
22
+
23
+ This ensures consistent handling in the UI layer without
24
+ needing tool-specific code.
25
+
26
+ Standard fields:
27
+ - success: bool - Whether execution succeeded
28
+ - error: Optional[str] - Error message if failed
29
+ - compute_time_seconds: float - Time taken by tool
30
+ - notes: Optional[str] - Additional info for user
31
+
32
+ File outputs (at least one should be provided on success):
33
+ - result_preview: Optional[str] - PNG/GIF preview for inline display
34
+ - result_origin: Optional[str] - Original format file for download
35
+ - result_path: Optional[str] - Backward compat field
36
+
37
+ Metadata:
38
+ - metadata_text: Optional[str] - Structured info about result
39
+ - metadata: Dict[str, Any] - Machine-readable metadata
40
+
41
+ Tracking:
42
+ - endpoint_url: str - API endpoint used
43
+ - api_name: str - API method called
44
+ """
45
+
46
+ # Core status
47
+ success: bool = False
48
+ error: Optional[str] = None
49
+ compute_time_seconds: float = 0.0
50
+
51
+ # File outputs (tools should provide these for UI to display/download)
52
+ result_preview: Optional[str] = Field(
53
+ default=None,
54
+ description="Path to preview image (PNG/GIF) for inline display in chat",
55
+ )
56
+ result_origin: Optional[str] = Field(
57
+ default=None,
58
+ description="Path to original format file (TIFF/NIfTI/DICOM) for download",
59
+ )
60
+ result_path: Optional[str] = Field(
61
+ default=None, description="Backward compatibility: primary result path"
62
+ )
63
+
64
+ # Metadata
65
+ metadata_text: Optional[str] = Field(
66
+ default=None, description="Human-readable metadata about the result"
67
+ )
68
+ metadata: Dict[str, Any] = Field(
69
+ default_factory=dict, description="Machine-readable metadata"
70
+ )
71
+ notes: Optional[str] = Field(
72
+ default=None, description="Additional notes or context for user"
73
+ )
74
+
75
+ # Tracking/debugging
76
+ endpoint_url: str = ""
77
+ api_name: str = ""
78
+
79
+
80
+ class ImageToolInput(BaseToolInput):
81
+ """
82
+ Common input pattern for image/volume processing tools.
83
+ """
84
+
85
+ image_path: str = Field(description="Path to the image/volume file")
86
+ description: Optional[str] = Field(
87
+ default=None, description="Optional context or notes from agent about the task"
88
+ )
src/ai_agent/agent/tools/mcp/lungs_segmentation_tool.py CHANGED
@@ -1,442 +1,442 @@
1
- from __future__ import annotations
2
-
3
- from typing import Optional, Any, Dict, Tuple
4
- import os
5
- import logging
6
- import tempfile
7
- from pathlib import Path
8
- import time
9
-
10
- import requests
11
- from gradio_client import Client, handle_file
12
-
13
- from ai_agent.utils.previews import _build_preview_for_vlm
14
- from ai_agent.utils.temp_file_manager import register_temp_file
15
- from ai_agent.agent.tools.mcp.registry import register_tool, ToolConfig
16
- from ai_agent.agent.tools.mcp.base import BaseToolOutput, ImageToolInput
17
-
18
- log = logging.getLogger("agent.lungs_segmentation")
19
-
20
-
21
- # ---------------------------------------------------------------------
22
- # Models
23
- # ---------------------------------------------------------------------
24
- class LungsSegmentationInput(ImageToolInput):
25
- """Input for 3D lungs segmentation tool."""
26
-
27
- pass # Inherits image_path and description from ImageToolInput
28
-
29
-
30
- class LungsSegmentationOutput(BaseToolOutput):
31
- """Output from 3D lungs segmentation tool."""
32
-
33
- # All standard fields inherited from BaseToolOutput:
34
- # - success, error, compute_time_seconds, notes
35
- # - result_preview, result_origin, result_path
36
- # - metadata_text, endpoint_url, api_name
37
- pass
38
-
39
-
40
- # ---------------------------------------------------------------------
41
- # Config
42
- # ---------------------------------------------------------------------
43
- LUNGS_SEGMENTATION_ENDPOINT = "https://qchapp-3d-lungs-segmentation.hf.space/"
44
- LUNGS_SEGMENTATION_API_NAME = "/segment"
45
-
46
- # Maximum file size for downloads (1GB for medical imaging)
47
- MAX_DOWNLOAD_SIZE = 1024 * 1024 * 1024 # 1GB in bytes
48
-
49
-
50
- # ---------------------------------------------------------------------
51
- # Public tool
52
- # ---------------------------------------------------------------------
53
- def tool_lungs_segmentation(inp: LungsSegmentationInput) -> LungsSegmentationOutput:
54
- """
55
- Run 3D lungs segmentation on a CT scan image via a Gradio Space.
56
-
57
- Materialization strategy (robust):
58
- 1) If Space returns dict FileData (url/path/etc) -> download via URL.
59
- 2) If Space returns URL string -> download.
60
- 3) If Space returns local file -> use it.
61
- 4) If Space returns server path (/tmp/...) -> try /gradio_api/file=... (may 403).
62
- """
63
- start_time = time.time()
64
-
65
- if not os.path.exists(inp.image_path):
66
- return LungsSegmentationOutput(
67
- success=False,
68
- error=f"Image file not found: {inp.image_path}",
69
- endpoint_url=LUNGS_SEGMENTATION_ENDPOINT,
70
- api_name=LUNGS_SEGMENTATION_API_NAME,
71
- )
72
-
73
- hf_token = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN")
74
-
75
- try:
76
- log.info(
77
- "Running lungs segmentation on %s (endpoint: %s)",
78
- inp.image_path,
79
- LUNGS_SEGMENTATION_ENDPOINT,
80
- )
81
-
82
- client = _make_gradio_client(LUNGS_SEGMENTATION_ENDPOINT, hf_token)
83
-
84
- # Call API
85
- try:
86
- result = client.predict(
87
- file_obj=handle_file(inp.image_path),
88
- api_name=LUNGS_SEGMENTATION_API_NAME,
89
- )
90
- log.info("API returned type=%s value=%r", type(result), result)
91
- except Exception as e:
92
- return LungsSegmentationOutput(
93
- success=False,
94
- error=f"API call failed: {e}",
95
- compute_time_seconds=time.time() - start_time,
96
- endpoint_url=LUNGS_SEGMENTATION_ENDPOINT,
97
- api_name=LUNGS_SEGMENTATION_API_NAME,
98
- )
99
-
100
- # Materialize to local file
101
- origin_path = _materialize_any(result, client=client, hf_token=hf_token)
102
-
103
- compute_time = time.time() - start_time
104
-
105
- if not origin_path or not os.path.exists(origin_path):
106
- # This is the common case if the Space returns '/tmp/...' and Gradio blocks it (403).
107
- return LungsSegmentationOutput(
108
- success=False,
109
- error="Could not materialize/download the result file.",
110
- compute_time_seconds=compute_time,
111
- endpoint_url=LUNGS_SEGMENTATION_ENDPOINT,
112
- api_name=LUNGS_SEGMENTATION_API_NAME,
113
- notes=(
114
- f"API returned: {result!r}. If this is a '/tmp/...' path and you see HTTP 403, "
115
- "the Space must return a FileData/url (recommended) or whitelist the output directory "
116
- "via allowed_paths / GRADIO_TEMP_DIR."
117
- ),
118
- )
119
-
120
- # Build preview + metadata using your shared function
121
- preview_path, meta_text = _safe_build_preview(origin_path)
122
-
123
- # Back-compat: prefer preview in result_path
124
- result_path = preview_path or origin_path
125
-
126
- return LungsSegmentationOutput(
127
- success=True,
128
- result_path=result_path,
129
- result_origin=origin_path,
130
- result_preview=preview_path,
131
- metadata_text=meta_text,
132
- compute_time_seconds=compute_time,
133
- endpoint_url=LUNGS_SEGMENTATION_ENDPOINT,
134
- api_name=LUNGS_SEGMENTATION_API_NAME,
135
- notes=f"Successfully segmented lungs from {os.path.basename(inp.image_path)}",
136
- )
137
-
138
- except Exception as e:
139
- log.exception("Lungs segmentation failed")
140
- return LungsSegmentationOutput(
141
- success=False,
142
- error=str(e),
143
- compute_time_seconds=time.time() - start_time,
144
- endpoint_url=LUNGS_SEGMENTATION_ENDPOINT,
145
- api_name=LUNGS_SEGMENTATION_API_NAME,
146
- )
147
-
148
-
149
- # ---------------------------------------------------------------------
150
- # Helpers
151
- # ---------------------------------------------------------------------
152
- def _make_gradio_client(endpoint: str, hf_token: Optional[str]) -> Client:
153
- """
154
- Create a gradio_client.Client with best compatibility across versions.
155
- """
156
- # Set extended timeout for both connection and operations (5 minutes for large files)
157
- httpx_kwargs = {"timeout": 300.0}
158
-
159
- # Newer versions use token=, older versions used hf_token=
160
- if hf_token:
161
- try:
162
- return Client(endpoint, hf_token=hf_token, httpx_kwargs=httpx_kwargs)
163
- except TypeError:
164
- # Fallback for very old versions without httpx_kwargs support
165
- try:
166
- return Client(endpoint, hf_token=hf_token)
167
- except TypeError:
168
- return Client(endpoint)
169
-
170
- try:
171
- return Client(endpoint, httpx_kwargs=httpx_kwargs)
172
- except TypeError:
173
- # Fallback for very old versions
174
- return Client(endpoint)
175
-
176
-
177
- def _safe_build_preview(origin_path: str) -> Tuple[Optional[str], Optional[str]]:
178
- """
179
- Wrapper around _build_preview_for_vlm so preview failures never break the tool.
180
- """
181
- try:
182
- preview_path, meta_text = _build_preview_for_vlm([origin_path])
183
- return preview_path, meta_text
184
- except Exception as e:
185
- log.debug("Preview build failed for %s: %r", origin_path, e)
186
- return None, None
187
-
188
-
189
- def _materialize_any(
190
- obj: Any, client: Client, hf_token: Optional[str] = None, _depth: int = 0
191
- ) -> Optional[str]:
192
- """
193
- Convert common Gradio outputs into a local file path.
194
-
195
- Supported:
196
- - local path string
197
- - URL string
198
- - dict (FileData-like) containing url/path/name/filepath
199
- - list/tuple containing any of the above
200
- - server path '/tmp/...' -> attempt Gradio file endpoint (may 403)
201
-
202
- Args:
203
- obj: Object to materialize
204
- client: Gradio client
205
- hf_token: Optional HuggingFace token
206
- _depth: Internal recursion depth counter (max 10)
207
- """
208
- if obj is None or _depth > 10:
209
- if _depth > 10:
210
- log.warning("Recursion depth limit reached in _materialize_any, halting.")
211
- return None
212
-
213
- # list/tuple: most Gradio outputs are single-element lists
214
- if isinstance(obj, (list, tuple)) and obj:
215
- return _materialize_any(
216
- obj[0], client=client, hf_token=hf_token, _depth=_depth + 1
217
- )
218
-
219
- # dict: FileData-like is best case (url provided)
220
- if isinstance(obj, dict):
221
- # Prefer URL if present
222
- url = obj.get("url")
223
- if isinstance(url, str) and url.startswith(("http://", "https://")):
224
- log.info("Materialize: dict url=%s", url)
225
- return _download_to_temp(url, hf_token=hf_token)
226
-
227
- # Fall back through common keys
228
- for k in ("path", "filepath", "file", "name"):
229
- v = obj.get(k)
230
- if isinstance(v, str) and v:
231
- return _materialize_any(
232
- v, client=client, hf_token=hf_token, _depth=_depth + 1
233
- )
234
-
235
- return None
236
-
237
- # string: local file, URL, or server path
238
- if isinstance(obj, str):
239
- s = obj.strip()
240
- if not s:
241
- return None
242
-
243
- # local file?
244
- p = Path(s)
245
- if p.exists() and p.is_file():
246
- log.info("Materialize: local file=%s", s)
247
- return str(p)
248
-
249
- # URL?
250
- if s.startswith(("http://", "https://")):
251
- log.info("Materialize: url=%s", s)
252
- return _download_to_temp(s, hf_token=hf_token)
253
-
254
- # server path? (e.g. /tmp/xxx_mask.tif)
255
- if s.startswith("/"):
256
- log.info("Materialize: server path=%s", s)
257
- return _download_from_gradio_file_endpoint(client, s, hf_token=hf_token)
258
-
259
- return None
260
-
261
-
262
- def _download_to_temp(url: str, hf_token: Optional[str] = None) -> Optional[str]:
263
- """
264
- Download a URL to a temporary file (streaming) with size limit checks.
265
- """
266
- headers: Dict[str, str] = {}
267
- if hf_token:
268
- headers["Authorization"] = f"Bearer {hf_token}"
269
-
270
- try:
271
- with requests.get(
272
- url, headers=headers, timeout=120, stream=True, allow_redirects=True
273
- ) as r:
274
- if r.status_code != 200:
275
- log.error("Download failed: url=%s status=%s", url, r.status_code)
276
- return None
277
-
278
- # Check Content-Length if available
279
- content_length = r.headers.get("content-length")
280
- if content_length and int(content_length) > MAX_DOWNLOAD_SIZE:
281
- log.error(
282
- "File too large: %s bytes (max %s)",
283
- content_length,
284
- MAX_DOWNLOAD_SIZE,
285
- )
286
- return None
287
-
288
- ext = _guess_ext(url, r.headers.get("content-type", ""))
289
-
290
- with tempfile.NamedTemporaryFile(
291
- delete=False, prefix="lungs_seg_", suffix=ext
292
- ) as f:
293
- downloaded_size = 0
294
- for chunk in r.iter_content(chunk_size=1024 * 1024):
295
- if chunk:
296
- downloaded_size += len(chunk)
297
- if downloaded_size > MAX_DOWNLOAD_SIZE:
298
- log.error(
299
- "Download exceeded size limit: %s bytes",
300
- downloaded_size,
301
- )
302
- f.close()
303
- os.remove(f.name)
304
- return None
305
- f.write(chunk)
306
- log.info("Downloaded %s bytes: %s -> %s", downloaded_size, url, f.name)
307
- return register_temp_file(f.name)
308
- except Exception as e:
309
- log.error("Failed to download %s: %r", url, e)
310
- return None
311
-
312
-
313
- def _download_from_gradio_file_endpoint(
314
- client: Client, server_path: str, hf_token: Optional[str] = None
315
- ) -> Optional[str]:
316
- """
317
- Last-resort fallback when API returns '/tmp/...' but no URL.
318
- Often blocked with 403 unless Space allows that directory or writes into Gradio temp/cache.
319
- Includes size limit checks.
320
- """
321
- base = (getattr(client, "src", None) or LUNGS_SEGMENTATION_ENDPOINT).rstrip("/")
322
- file_url = f"{base}/gradio_api/file={server_path}"
323
-
324
- headers: Dict[str, str] = {}
325
- if hf_token:
326
- headers["Authorization"] = f"Bearer {hf_token}"
327
-
328
- params: Dict[str, str] = {}
329
- session_hash = getattr(client, "session_hash", None)
330
- if session_hash:
331
- params["session_hash"] = session_hash
332
-
333
- try:
334
- r = requests.get(
335
- file_url, headers=headers, params=params, timeout=60, stream=True
336
- )
337
- if r.status_code == 403:
338
- # Common: file exists but not allowed to be served
339
- detail: Any
340
- try:
341
- detail = r.json()
342
- except Exception:
343
- detail = r.text[:200]
344
- log.error("HTTP 403 from %s detail=%r", file_url, detail)
345
- return None
346
-
347
- if r.status_code != 200:
348
- log.error("HTTP %s from %s", r.status_code, file_url)
349
- return None
350
-
351
- # Check Content-Length before downloading
352
- content_length = r.headers.get("content-length")
353
- if content_length and int(content_length) > MAX_DOWNLOAD_SIZE:
354
- log.error(
355
- "File too large: %s bytes (max %s)", content_length, MAX_DOWNLOAD_SIZE
356
- )
357
- return None
358
-
359
- # Read content with size check
360
- content = b""
361
- for chunk in r.iter_content(chunk_size=1024 * 1024):
362
- if chunk:
363
- content += chunk
364
- if len(content) > MAX_DOWNLOAD_SIZE:
365
- log.error("Download exceeded size limit: %s bytes", len(content))
366
- return None
367
-
368
- ct = r.headers.get("content-type", "")
369
- if "html" in ct.lower() or content.startswith(b"<!"):
370
- log.error("Got HTML instead of file from %s", file_url)
371
- return None
372
-
373
- ext = os.path.splitext(server_path)[1] or ".tif"
374
- with tempfile.NamedTemporaryFile(
375
- delete=False, prefix="lungs_seg_", suffix=ext
376
- ) as f:
377
- f.write(content)
378
- log.info(
379
- "Downloaded %s bytes from gradio file endpoint -> %s",
380
- len(content),
381
- f.name,
382
- )
383
- return register_temp_file(f.name)
384
-
385
- except Exception as e:
386
- log.error("Failed gradio file endpoint download: %r", e)
387
- return None
388
-
389
-
390
- def _guess_ext(url: str, content_type: str) -> str:
391
- """
392
- Guess file extension from URL path or Content-Type.
393
- """
394
- from urllib.parse import urlparse
395
-
396
- path = urlparse(url).path.lower()
397
-
398
- if path.endswith(".nii.gz"):
399
- return ".nii.gz"
400
-
401
- ext = os.path.splitext(path)[1]
402
- if ext:
403
- return ext
404
-
405
- ct = (content_type or "").lower()
406
- if "tiff" in ct or "tif" in ct:
407
- return ".tif"
408
- if "png" in ct:
409
- return ".png"
410
- if "jpeg" in ct or "jpg" in ct:
411
- return ".jpg"
412
- if "gif" in ct:
413
- return ".gif"
414
- if "nifti" in ct or "nii" in ct:
415
- return ".nii.gz"
416
- return ".bin"
417
-
418
-
419
- # ---------------------------------------------------------------------
420
- # Tool Registration
421
- # ---------------------------------------------------------------------
422
- register_tool(
423
- ToolConfig(
424
- name="lungs_segmentation",
425
- display_name="3D Lungs Segmentation",
426
- icon="🫁",
427
- catalog_names=["lungs-segmentation"], # Catalog name from dataset/catalog.jsonl
428
- input_model=LungsSegmentationInput,
429
- output_model=LungsSegmentationOutput,
430
- executor=tool_lungs_segmentation,
431
- supports_images=True,
432
- supports_files=True,
433
- requires_approval=True,
434
- preview_field="result_preview",
435
- download_fields="result_origin", # Could also be ["result_origin", "other_file"]
436
- metadata_field="metadata_text",
437
- notes_field="notes",
438
- success_field="success",
439
- error_field="error",
440
- compute_time_field="compute_time_seconds",
441
- )
442
- )
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, Any, Dict, Tuple
4
+ import os
5
+ import logging
6
+ import tempfile
7
+ from pathlib import Path
8
+ import time
9
+
10
+ import requests
11
+ from gradio_client import Client, handle_file
12
+
13
+ from ai_agent.utils.previews import _build_preview_for_vlm
14
+ from ai_agent.utils.temp_file_manager import register_temp_file
15
+ from ai_agent.agent.tools.mcp.registry import register_tool, ToolConfig
16
+ from ai_agent.agent.tools.mcp.base import BaseToolOutput, ImageToolInput
17
+
18
+ log = logging.getLogger("agent.lungs_segmentation")
19
+
20
+
21
+ # ---------------------------------------------------------------------
22
+ # Models
23
+ # ---------------------------------------------------------------------
24
+ class LungsSegmentationInput(ImageToolInput):
25
+ """Input for 3D lungs segmentation tool."""
26
+
27
+ pass # Inherits image_path and description from ImageToolInput
28
+
29
+
30
+ class LungsSegmentationOutput(BaseToolOutput):
31
+ """Output from 3D lungs segmentation tool."""
32
+
33
+ # All standard fields inherited from BaseToolOutput:
34
+ # - success, error, compute_time_seconds, notes
35
+ # - result_preview, result_origin, result_path
36
+ # - metadata_text, endpoint_url, api_name
37
+ pass
38
+
39
+
40
+ # ---------------------------------------------------------------------
41
+ # Config
42
+ # ---------------------------------------------------------------------
43
+ LUNGS_SEGMENTATION_ENDPOINT = "https://qchapp-3d-lungs-segmentation.hf.space/"
44
+ LUNGS_SEGMENTATION_API_NAME = "/segment"
45
+
46
+ # Maximum file size for downloads (1GB for medical imaging)
47
+ MAX_DOWNLOAD_SIZE = 1024 * 1024 * 1024 # 1GB in bytes
48
+
49
+
50
+ # ---------------------------------------------------------------------
51
+ # Public tool
52
+ # ---------------------------------------------------------------------
53
+ def tool_lungs_segmentation(inp: LungsSegmentationInput) -> LungsSegmentationOutput:
54
+ """
55
+ Run 3D lungs segmentation on a CT scan image via a Gradio Space.
56
+
57
+ Materialization strategy (robust):
58
+ 1) If Space returns dict FileData (url/path/etc) -> download via URL.
59
+ 2) If Space returns URL string -> download.
60
+ 3) If Space returns local file -> use it.
61
+ 4) If Space returns server path (/tmp/...) -> try /gradio_api/file=... (may 403).
62
+ """
63
+ start_time = time.time()
64
+
65
+ if not os.path.exists(inp.image_path):
66
+ return LungsSegmentationOutput(
67
+ success=False,
68
+ error=f"Image file not found: {inp.image_path}",
69
+ endpoint_url=LUNGS_SEGMENTATION_ENDPOINT,
70
+ api_name=LUNGS_SEGMENTATION_API_NAME,
71
+ )
72
+
73
+ hf_token = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN")
74
+
75
+ try:
76
+ log.info(
77
+ "Running lungs segmentation on %s (endpoint: %s)",
78
+ inp.image_path,
79
+ LUNGS_SEGMENTATION_ENDPOINT,
80
+ )
81
+
82
+ client = _make_gradio_client(LUNGS_SEGMENTATION_ENDPOINT, hf_token)
83
+
84
+ # Call API
85
+ try:
86
+ result = client.predict(
87
+ file_obj=handle_file(inp.image_path),
88
+ api_name=LUNGS_SEGMENTATION_API_NAME,
89
+ )
90
+ log.info("API returned type=%s value=%r", type(result), result)
91
+ except Exception as e:
92
+ return LungsSegmentationOutput(
93
+ success=False,
94
+ error=f"API call failed: {e}",
95
+ compute_time_seconds=time.time() - start_time,
96
+ endpoint_url=LUNGS_SEGMENTATION_ENDPOINT,
97
+ api_name=LUNGS_SEGMENTATION_API_NAME,
98
+ )
99
+
100
+ # Materialize to local file
101
+ origin_path = _materialize_any(result, client=client, hf_token=hf_token)
102
+
103
+ compute_time = time.time() - start_time
104
+
105
+ if not origin_path or not os.path.exists(origin_path):
106
+ # This is the common case if the Space returns '/tmp/...' and Gradio blocks it (403).
107
+ return LungsSegmentationOutput(
108
+ success=False,
109
+ error="Could not materialize/download the result file.",
110
+ compute_time_seconds=compute_time,
111
+ endpoint_url=LUNGS_SEGMENTATION_ENDPOINT,
112
+ api_name=LUNGS_SEGMENTATION_API_NAME,
113
+ notes=(
114
+ f"API returned: {result!r}. If this is a '/tmp/...' path and you see HTTP 403, "
115
+ "the Space must return a FileData/url (recommended) or whitelist the output directory "
116
+ "via allowed_paths / GRADIO_TEMP_DIR."
117
+ ),
118
+ )
119
+
120
+ # Build preview + metadata using your shared function
121
+ preview_path, meta_text = _safe_build_preview(origin_path)
122
+
123
+ # Back-compat: prefer preview in result_path
124
+ result_path = preview_path or origin_path
125
+
126
+ return LungsSegmentationOutput(
127
+ success=True,
128
+ result_path=result_path,
129
+ result_origin=origin_path,
130
+ result_preview=preview_path,
131
+ metadata_text=meta_text,
132
+ compute_time_seconds=compute_time,
133
+ endpoint_url=LUNGS_SEGMENTATION_ENDPOINT,
134
+ api_name=LUNGS_SEGMENTATION_API_NAME,
135
+ notes=f"Successfully segmented lungs from {os.path.basename(inp.image_path)}",
136
+ )
137
+
138
+ except Exception as e:
139
+ log.exception("Lungs segmentation failed")
140
+ return LungsSegmentationOutput(
141
+ success=False,
142
+ error=str(e),
143
+ compute_time_seconds=time.time() - start_time,
144
+ endpoint_url=LUNGS_SEGMENTATION_ENDPOINT,
145
+ api_name=LUNGS_SEGMENTATION_API_NAME,
146
+ )
147
+
148
+
149
+ # ---------------------------------------------------------------------
150
+ # Helpers
151
+ # ---------------------------------------------------------------------
152
+ def _make_gradio_client(endpoint: str, hf_token: Optional[str]) -> Client:
153
+ """
154
+ Create a gradio_client.Client with best compatibility across versions.
155
+ """
156
+ # Set extended timeout for both connection and operations (5 minutes for large files)
157
+ httpx_kwargs = {"timeout": 300.0}
158
+
159
+ # Newer versions use token=, older versions used hf_token=
160
+ if hf_token:
161
+ try:
162
+ return Client(endpoint, hf_token=hf_token, httpx_kwargs=httpx_kwargs)
163
+ except TypeError:
164
+ # Fallback for very old versions without httpx_kwargs support
165
+ try:
166
+ return Client(endpoint, hf_token=hf_token)
167
+ except TypeError:
168
+ return Client(endpoint)
169
+
170
+ try:
171
+ return Client(endpoint, httpx_kwargs=httpx_kwargs)
172
+ except TypeError:
173
+ # Fallback for very old versions
174
+ return Client(endpoint)
175
+
176
+
177
+ def _safe_build_preview(origin_path: str) -> Tuple[Optional[str], Optional[str]]:
178
+ """
179
+ Wrapper around _build_preview_for_vlm so preview failures never break the tool.
180
+ """
181
+ try:
182
+ preview_path, meta_text = _build_preview_for_vlm([origin_path])
183
+ return preview_path, meta_text
184
+ except Exception as e:
185
+ log.debug("Preview build failed for %s: %r", origin_path, e)
186
+ return None, None
187
+
188
+
189
+ def _materialize_any(
190
+ obj: Any, client: Client, hf_token: Optional[str] = None, _depth: int = 0
191
+ ) -> Optional[str]:
192
+ """
193
+ Convert common Gradio outputs into a local file path.
194
+
195
+ Supported:
196
+ - local path string
197
+ - URL string
198
+ - dict (FileData-like) containing url/path/name/filepath
199
+ - list/tuple containing any of the above
200
+ - server path '/tmp/...' -> attempt Gradio file endpoint (may 403)
201
+
202
+ Args:
203
+ obj: Object to materialize
204
+ client: Gradio client
205
+ hf_token: Optional HuggingFace token
206
+ _depth: Internal recursion depth counter (max 10)
207
+ """
208
+ if obj is None or _depth > 10:
209
+ if _depth > 10:
210
+ log.warning("Recursion depth limit reached in _materialize_any, halting.")
211
+ return None
212
+
213
+ # list/tuple: most Gradio outputs are single-element lists
214
+ if isinstance(obj, (list, tuple)) and obj:
215
+ return _materialize_any(
216
+ obj[0], client=client, hf_token=hf_token, _depth=_depth + 1
217
+ )
218
+
219
+ # dict: FileData-like is best case (url provided)
220
+ if isinstance(obj, dict):
221
+ # Prefer URL if present
222
+ url = obj.get("url")
223
+ if isinstance(url, str) and url.startswith(("http://", "https://")):
224
+ log.info("Materialize: dict url=%s", url)
225
+ return _download_to_temp(url, hf_token=hf_token)
226
+
227
+ # Fall back through common keys
228
+ for k in ("path", "filepath", "file", "name"):
229
+ v = obj.get(k)
230
+ if isinstance(v, str) and v:
231
+ return _materialize_any(
232
+ v, client=client, hf_token=hf_token, _depth=_depth + 1
233
+ )
234
+
235
+ return None
236
+
237
+ # string: local file, URL, or server path
238
+ if isinstance(obj, str):
239
+ s = obj.strip()
240
+ if not s:
241
+ return None
242
+
243
+ # local file?
244
+ p = Path(s)
245
+ if p.exists() and p.is_file():
246
+ log.info("Materialize: local file=%s", s)
247
+ return str(p)
248
+
249
+ # URL?
250
+ if s.startswith(("http://", "https://")):
251
+ log.info("Materialize: url=%s", s)
252
+ return _download_to_temp(s, hf_token=hf_token)
253
+
254
+ # server path? (e.g. /tmp/xxx_mask.tif)
255
+ if s.startswith("/"):
256
+ log.info("Materialize: server path=%s", s)
257
+ return _download_from_gradio_file_endpoint(client, s, hf_token=hf_token)
258
+
259
+ return None
260
+
261
+
262
+ def _download_to_temp(url: str, hf_token: Optional[str] = None) -> Optional[str]:
263
+ """
264
+ Download a URL to a temporary file (streaming) with size limit checks.
265
+ """
266
+ headers: Dict[str, str] = {}
267
+ if hf_token:
268
+ headers["Authorization"] = f"Bearer {hf_token}"
269
+
270
+ try:
271
+ with requests.get(
272
+ url, headers=headers, timeout=120, stream=True, allow_redirects=True
273
+ ) as r:
274
+ if r.status_code != 200:
275
+ log.error("Download failed: url=%s status=%s", url, r.status_code)
276
+ return None
277
+
278
+ # Check Content-Length if available
279
+ content_length = r.headers.get("content-length")
280
+ if content_length and int(content_length) > MAX_DOWNLOAD_SIZE:
281
+ log.error(
282
+ "File too large: %s bytes (max %s)",
283
+ content_length,
284
+ MAX_DOWNLOAD_SIZE,
285
+ )
286
+ return None
287
+
288
+ ext = _guess_ext(url, r.headers.get("content-type", ""))
289
+
290
+ with tempfile.NamedTemporaryFile(
291
+ delete=False, prefix="lungs_seg_", suffix=ext
292
+ ) as f:
293
+ downloaded_size = 0
294
+ for chunk in r.iter_content(chunk_size=1024 * 1024):
295
+ if chunk:
296
+ downloaded_size += len(chunk)
297
+ if downloaded_size > MAX_DOWNLOAD_SIZE:
298
+ log.error(
299
+ "Download exceeded size limit: %s bytes",
300
+ downloaded_size,
301
+ )
302
+ f.close()
303
+ os.remove(f.name)
304
+ return None
305
+ f.write(chunk)
306
+ log.info("Downloaded %s bytes: %s -> %s", downloaded_size, url, f.name)
307
+ return register_temp_file(f.name)
308
+ except Exception as e:
309
+ log.error("Failed to download %s: %r", url, e)
310
+ return None
311
+
312
+
313
+ def _download_from_gradio_file_endpoint(
314
+ client: Client, server_path: str, hf_token: Optional[str] = None
315
+ ) -> Optional[str]:
316
+ """
317
+ Last-resort fallback when API returns '/tmp/...' but no URL.
318
+ Often blocked with 403 unless Space allows that directory or writes into Gradio temp/cache.
319
+ Includes size limit checks.
320
+ """
321
+ base = (getattr(client, "src", None) or LUNGS_SEGMENTATION_ENDPOINT).rstrip("/")
322
+ file_url = f"{base}/gradio_api/file={server_path}"
323
+
324
+ headers: Dict[str, str] = {}
325
+ if hf_token:
326
+ headers["Authorization"] = f"Bearer {hf_token}"
327
+
328
+ params: Dict[str, str] = {}
329
+ session_hash = getattr(client, "session_hash", None)
330
+ if session_hash:
331
+ params["session_hash"] = session_hash
332
+
333
+ try:
334
+ r = requests.get(
335
+ file_url, headers=headers, params=params, timeout=60, stream=True
336
+ )
337
+ if r.status_code == 403:
338
+ # Common: file exists but not allowed to be served
339
+ detail: Any
340
+ try:
341
+ detail = r.json()
342
+ except Exception:
343
+ detail = r.text[:200]
344
+ log.error("HTTP 403 from %s detail=%r", file_url, detail)
345
+ return None
346
+
347
+ if r.status_code != 200:
348
+ log.error("HTTP %s from %s", r.status_code, file_url)
349
+ return None
350
+
351
+ # Check Content-Length before downloading
352
+ content_length = r.headers.get("content-length")
353
+ if content_length and int(content_length) > MAX_DOWNLOAD_SIZE:
354
+ log.error(
355
+ "File too large: %s bytes (max %s)", content_length, MAX_DOWNLOAD_SIZE
356
+ )
357
+ return None
358
+
359
+ # Read content with size check
360
+ content = b""
361
+ for chunk in r.iter_content(chunk_size=1024 * 1024):
362
+ if chunk:
363
+ content += chunk
364
+ if len(content) > MAX_DOWNLOAD_SIZE:
365
+ log.error("Download exceeded size limit: %s bytes", len(content))
366
+ return None
367
+
368
+ ct = r.headers.get("content-type", "")
369
+ if "html" in ct.lower() or content.startswith(b"<!"):
370
+ log.error("Got HTML instead of file from %s", file_url)
371
+ return None
372
+
373
+ ext = os.path.splitext(server_path)[1] or ".tif"
374
+ with tempfile.NamedTemporaryFile(
375
+ delete=False, prefix="lungs_seg_", suffix=ext
376
+ ) as f:
377
+ f.write(content)
378
+ log.info(
379
+ "Downloaded %s bytes from gradio file endpoint -> %s",
380
+ len(content),
381
+ f.name,
382
+ )
383
+ return register_temp_file(f.name)
384
+
385
+ except Exception as e:
386
+ log.error("Failed gradio file endpoint download: %r", e)
387
+ return None
388
+
389
+
390
+ def _guess_ext(url: str, content_type: str) -> str:
391
+ """
392
+ Guess file extension from URL path or Content-Type.
393
+ """
394
+ from urllib.parse import urlparse
395
+
396
+ path = urlparse(url).path.lower()
397
+
398
+ if path.endswith(".nii.gz"):
399
+ return ".nii.gz"
400
+
401
+ ext = os.path.splitext(path)[1]
402
+ if ext:
403
+ return ext
404
+
405
+ ct = (content_type or "").lower()
406
+ if "tiff" in ct or "tif" in ct:
407
+ return ".tif"
408
+ if "png" in ct:
409
+ return ".png"
410
+ if "jpeg" in ct or "jpg" in ct:
411
+ return ".jpg"
412
+ if "gif" in ct:
413
+ return ".gif"
414
+ if "nifti" in ct or "nii" in ct:
415
+ return ".nii.gz"
416
+ return ".bin"
417
+
418
+
419
+ # ---------------------------------------------------------------------
420
+ # Tool Registration
421
+ # ---------------------------------------------------------------------
422
+ register_tool(
423
+ ToolConfig(
424
+ name="lungs_segmentation",
425
+ display_name="3D Lungs Segmentation",
426
+ icon="🫁",
427
+ catalog_names=["lungs-segmentation"], # Catalog name from dataset/catalog.jsonl
428
+ input_model=LungsSegmentationInput,
429
+ output_model=LungsSegmentationOutput,
430
+ executor=tool_lungs_segmentation,
431
+ supports_images=True,
432
+ supports_files=True,
433
+ requires_approval=True,
434
+ preview_field="result_preview",
435
+ download_fields="result_origin", # Could also be ["result_origin", "other_file"]
436
+ metadata_field="metadata_text",
437
+ notes_field="notes",
438
+ success_field="success",
439
+ error_field="error",
440
+ compute_time_field="compute_time_seconds",
441
+ )
442
+ )
src/ai_agent/agent/tools/mcp/registry.py CHANGED
@@ -1,203 +1,203 @@
1
- from __future__ import annotations
2
-
3
- from typing import Dict, Type, Callable, Optional, List, Any
4
- from pydantic import BaseModel
5
- from dataclasses import dataclass
6
-
7
-
8
- @dataclass
9
- class ToolConfig:
10
- """
11
- Declarative configuration for a tool.
12
-
13
- Tools register themselves with this config, and the UI uses it
14
- to generically handle execution, display, and file management.
15
- """
16
-
17
- # Core identification
18
- name: str # Internal name (e.g., "lungs_segmentation")
19
- display_name: str # User-facing name (e.g., "3D Lungs Segmentation")
20
- icon: str # Emoji for UI display
21
-
22
- # Type information
23
- input_model: Type[BaseModel] # Pydantic model for inputs
24
- output_model: Type[BaseModel] # Pydantic model for outputs
25
- executor: Callable # Function that takes input_model and returns output_model
26
-
27
- # Capability flags
28
- catalog_names: Optional[List[str]] = (
29
- None # Catalog names for this tool (e.g., ["lungs-segmentation"])
30
- )
31
- supports_images: bool = True
32
- supports_files: bool = True
33
- requires_approval: bool = True # Whether to show approval button
34
-
35
- # Output field mappings (how to extract results from output_model)
36
- # These map generic concepts to tool-specific field names
37
- preview_field: str = "result_preview" # Field containing preview image path
38
- download_fields: List[str] | str = (
39
- "result_origin" # Field(s) for downloadable files
40
- )
41
- metadata_field: Optional[str] = "metadata_text" # Optional metadata text
42
- notes_field: str = "notes" # Field containing execution notes
43
-
44
- # Success detection
45
- success_field: str = "success" # Field indicating success/failure
46
- error_field: str = "error" # Field containing error message
47
- compute_time_field: str = "compute_time_seconds" # Field with timing info
48
-
49
-
50
- # Global tool registry
51
- TOOL_REGISTRY: Dict[str, ToolConfig] = {}
52
-
53
- # Reverse mapping from catalog names to tool names
54
- CATALOG_NAME_TO_TOOL: Dict[str, str] = {}
55
-
56
-
57
- def register_tool(config: ToolConfig) -> None:
58
- """
59
- Register a tool with the global registry.
60
-
61
- Args:
62
- config: Tool configuration
63
-
64
- Raises:
65
- ValueError: If tool name already registered or catalog name collision
66
- """
67
- if config.name in TOOL_REGISTRY:
68
- raise ValueError(f"Tool '{config.name}' is already registered")
69
-
70
- # Check for catalog name collisions before registering
71
- if config.catalog_names:
72
- for catalog_name in config.catalog_names:
73
- if (
74
- catalog_name in CATALOG_NAME_TO_TOOL
75
- and CATALOG_NAME_TO_TOOL[catalog_name] != config.name
76
- ):
77
- raise ValueError(
78
- f"Catalog name '{catalog_name}' already registered to "
79
- f"'{CATALOG_NAME_TO_TOOL[catalog_name]}'"
80
- )
81
-
82
- TOOL_REGISTRY[config.name] = config
83
-
84
- # Register catalog name mappings
85
- if config.catalog_names:
86
- for catalog_name in config.catalog_names:
87
- CATALOG_NAME_TO_TOOL[catalog_name] = config.name
88
-
89
-
90
- def get_tool(name: str) -> Optional[ToolConfig]:
91
- """
92
- Get tool configuration by name.
93
-
94
- Args:
95
- name: Tool name (registry name or catalog name)
96
-
97
- Returns:
98
- ToolConfig if found, None otherwise
99
-
100
- Note:
101
- This function checks both the tool registry name and catalog names.
102
- """
103
- # First try direct registry lookup
104
- config = TOOL_REGISTRY.get(name)
105
- if config:
106
- return config
107
-
108
- # Try catalog name mapping
109
- tool_name = CATALOG_NAME_TO_TOOL.get(name)
110
- if tool_name:
111
- return TOOL_REGISTRY.get(tool_name)
112
-
113
- return None
114
-
115
-
116
- def list_tools() -> List[str]:
117
- """Get list of all registered tool names."""
118
- return list(TOOL_REGISTRY.keys())
119
-
120
-
121
- def get_tool_display_name(name: str) -> str:
122
- """
123
- Get display name for a tool, with fallback to name.
124
-
125
- Args:
126
- name: Tool name
127
-
128
- Returns:
129
- Display name or formatted version of name
130
- """
131
- tool = get_tool(name)
132
- if tool:
133
- return tool.display_name
134
- # Fallback: format name nicely
135
- return name.replace("_", " ").title()
136
-
137
-
138
- def get_tool_icon(name: str) -> str:
139
- """
140
- Get icon for a tool, with fallback.
141
-
142
- Args:
143
- name: Tool name
144
-
145
- Returns:
146
- Icon emoji or default
147
- """
148
- tool = get_tool(name)
149
- if tool:
150
- return tool.icon
151
- return "🔧" # Default tool icon
152
-
153
-
154
- def extract_output_field(output: BaseModel, field_name: str) -> Any:
155
- """
156
- Safely extract a field from tool output.
157
-
158
- Args:
159
- output: Tool output object
160
- field_name: Field name to extract
161
-
162
- Returns:
163
- Field value or None if not found
164
- """
165
- return getattr(output, field_name, None)
166
-
167
-
168
- def extract_preview(output: BaseModel, tool_name: str) -> Optional[str]:
169
- """Extract preview image path from tool output."""
170
- tool = get_tool(tool_name)
171
- if not tool:
172
- return None
173
- return extract_output_field(output, tool.preview_field)
174
-
175
-
176
- def extract_downloads(output: BaseModel, tool_name: str) -> List[str]:
177
- """Extract downloadable file paths from tool output."""
178
- tool = get_tool(tool_name)
179
- if not tool:
180
- return []
181
-
182
- download_fields = tool.download_fields
183
- if isinstance(download_fields, str):
184
- download_fields = [download_fields]
185
-
186
- downloads = []
187
- for field in download_fields:
188
- value = extract_output_field(output, field)
189
- if value:
190
- if isinstance(value, list):
191
- downloads.extend([v for v in value if v])
192
- elif isinstance(value, str):
193
- downloads.append(value)
194
-
195
- return [d for d in downloads if d] # Filter None/empty
196
-
197
-
198
- def extract_metadata(output: BaseModel, tool_name: str) -> Optional[str]:
199
- """Extract metadata text from tool output."""
200
- tool = get_tool(tool_name)
201
- if not tool or not tool.metadata_field:
202
- return None
203
- return extract_output_field(output, tool.metadata_field)
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict, Type, Callable, Optional, List, Any
4
+ from pydantic import BaseModel
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass
9
+ class ToolConfig:
10
+ """
11
+ Declarative configuration for a tool.
12
+
13
+ Tools register themselves with this config, and the UI uses it
14
+ to generically handle execution, display, and file management.
15
+ """
16
+
17
+ # Core identification
18
+ name: str # Internal name (e.g., "lungs_segmentation")
19
+ display_name: str # User-facing name (e.g., "3D Lungs Segmentation")
20
+ icon: str # Emoji for UI display
21
+
22
+ # Type information
23
+ input_model: Type[BaseModel] # Pydantic model for inputs
24
+ output_model: Type[BaseModel] # Pydantic model for outputs
25
+ executor: Callable # Function that takes input_model and returns output_model
26
+
27
+ # Capability flags
28
+ catalog_names: Optional[List[str]] = (
29
+ None # Catalog names for this tool (e.g., ["lungs-segmentation"])
30
+ )
31
+ supports_images: bool = True
32
+ supports_files: bool = True
33
+ requires_approval: bool = True # Whether to show approval button
34
+
35
+ # Output field mappings (how to extract results from output_model)
36
+ # These map generic concepts to tool-specific field names
37
+ preview_field: str = "result_preview" # Field containing preview image path
38
+ download_fields: List[str] | str = (
39
+ "result_origin" # Field(s) for downloadable files
40
+ )
41
+ metadata_field: Optional[str] = "metadata_text" # Optional metadata text
42
+ notes_field: str = "notes" # Field containing execution notes
43
+
44
+ # Success detection
45
+ success_field: str = "success" # Field indicating success/failure
46
+ error_field: str = "error" # Field containing error message
47
+ compute_time_field: str = "compute_time_seconds" # Field with timing info
48
+
49
+
50
+ # Global tool registry
51
+ TOOL_REGISTRY: Dict[str, ToolConfig] = {}
52
+
53
+ # Reverse mapping from catalog names to tool names
54
+ CATALOG_NAME_TO_TOOL: Dict[str, str] = {}
55
+
56
+
57
+ def register_tool(config: ToolConfig) -> None:
58
+ """
59
+ Register a tool with the global registry.
60
+
61
+ Args:
62
+ config: Tool configuration
63
+
64
+ Raises:
65
+ ValueError: If tool name already registered or catalog name collision
66
+ """
67
+ if config.name in TOOL_REGISTRY:
68
+ raise ValueError(f"Tool '{config.name}' is already registered")
69
+
70
+ # Check for catalog name collisions before registering
71
+ if config.catalog_names:
72
+ for catalog_name in config.catalog_names:
73
+ if (
74
+ catalog_name in CATALOG_NAME_TO_TOOL
75
+ and CATALOG_NAME_TO_TOOL[catalog_name] != config.name
76
+ ):
77
+ raise ValueError(
78
+ f"Catalog name '{catalog_name}' already registered to "
79
+ f"'{CATALOG_NAME_TO_TOOL[catalog_name]}'"
80
+ )
81
+
82
+ TOOL_REGISTRY[config.name] = config
83
+
84
+ # Register catalog name mappings
85
+ if config.catalog_names:
86
+ for catalog_name in config.catalog_names:
87
+ CATALOG_NAME_TO_TOOL[catalog_name] = config.name
88
+
89
+
90
+ def get_tool(name: str) -> Optional[ToolConfig]:
91
+ """
92
+ Get tool configuration by name.
93
+
94
+ Args:
95
+ name: Tool name (registry name or catalog name)
96
+
97
+ Returns:
98
+ ToolConfig if found, None otherwise
99
+
100
+ Note:
101
+ This function checks both the tool registry name and catalog names.
102
+ """
103
+ # First try direct registry lookup
104
+ config = TOOL_REGISTRY.get(name)
105
+ if config:
106
+ return config
107
+
108
+ # Try catalog name mapping
109
+ tool_name = CATALOG_NAME_TO_TOOL.get(name)
110
+ if tool_name:
111
+ return TOOL_REGISTRY.get(tool_name)
112
+
113
+ return None
114
+
115
+
116
+ def list_tools() -> List[str]:
117
+ """Get list of all registered tool names."""
118
+ return list(TOOL_REGISTRY.keys())
119
+
120
+
121
+ def get_tool_display_name(name: str) -> str:
122
+ """
123
+ Get display name for a tool, with fallback to name.
124
+
125
+ Args:
126
+ name: Tool name
127
+
128
+ Returns:
129
+ Display name or formatted version of name
130
+ """
131
+ tool = get_tool(name)
132
+ if tool:
133
+ return tool.display_name
134
+ # Fallback: format name nicely
135
+ return name.replace("_", " ").title()
136
+
137
+
138
+ def get_tool_icon(name: str) -> str:
139
+ """
140
+ Get icon for a tool, with fallback.
141
+
142
+ Args:
143
+ name: Tool name
144
+
145
+ Returns:
146
+ Icon emoji or default
147
+ """
148
+ tool = get_tool(name)
149
+ if tool:
150
+ return tool.icon
151
+ return "🔧" # Default tool icon
152
+
153
+
154
+ def extract_output_field(output: BaseModel, field_name: str) -> Any:
155
+ """
156
+ Safely extract a field from tool output.
157
+
158
+ Args:
159
+ output: Tool output object
160
+ field_name: Field name to extract
161
+
162
+ Returns:
163
+ Field value or None if not found
164
+ """
165
+ return getattr(output, field_name, None)
166
+
167
+
168
+ def extract_preview(output: BaseModel, tool_name: str) -> Optional[str]:
169
+ """Extract preview image path from tool output."""
170
+ tool = get_tool(tool_name)
171
+ if not tool:
172
+ return None
173
+ return extract_output_field(output, tool.preview_field)
174
+
175
+
176
+ def extract_downloads(output: BaseModel, tool_name: str) -> List[str]:
177
+ """Extract downloadable file paths from tool output."""
178
+ tool = get_tool(tool_name)
179
+ if not tool:
180
+ return []
181
+
182
+ download_fields = tool.download_fields
183
+ if isinstance(download_fields, str):
184
+ download_fields = [download_fields]
185
+
186
+ downloads = []
187
+ for field in download_fields:
188
+ value = extract_output_field(output, field)
189
+ if value:
190
+ if isinstance(value, list):
191
+ downloads.extend([v for v in value if v])
192
+ elif isinstance(value, str):
193
+ downloads.append(value)
194
+
195
+ return [d for d in downloads if d] # Filter None/empty
196
+
197
+
198
+ def extract_metadata(output: BaseModel, tool_name: str) -> Optional[str]:
199
+ """Extract metadata text from tool output."""
200
+ tool = get_tool(tool_name)
201
+ if not tool or not tool.metadata_field:
202
+ return None
203
+ return extract_output_field(output, tool.metadata_field)
src/ai_agent/agent/tools/query_utils.py CHANGED
@@ -1,128 +1,128 @@
1
- from __future__ import annotations
2
-
3
- import re
4
- from typing import List
5
-
6
- FORMAT_TOKEN_MAP = {
7
- "tif": "TIFF",
8
- "tiff": "TIFF",
9
- "nii": "NIfTI",
10
- "nii.gz": "NIfTI",
11
- "dcm": "DICOM",
12
- "dicom": "DICOM",
13
- "nrrd": "NRRD",
14
- "png": "PNG",
15
- "jpg": "JPEG",
16
- "jpeg": "JPEG",
17
- }
18
-
19
- _REPO_DRIFT_TERMS = {
20
- "github",
21
- "repository",
22
- "repo",
23
- "official",
24
- "readme",
25
- "docs",
26
- "documentation",
27
- "source",
28
- "sourcecode",
29
- }
30
-
31
- _LOW_SIGNAL_TERMS = _REPO_DRIFT_TERMS | {
32
- "tool",
33
- "tools",
34
- "project",
35
- "framework",
36
- }
37
-
38
-
39
- def _tokenize_query(query: str) -> List[str]:
40
- return [t for t in re.findall(r"[a-z0-9_+-]+", (query or "").lower()) if t]
41
-
42
-
43
- def normalize_formats(formats: List[str]) -> List[str]:
44
- seen = set()
45
- out: List[str] = []
46
- for ext in formats:
47
- norm = (ext or "").strip().lower()
48
- if not norm or norm in seen:
49
- continue
50
- seen.add(norm)
51
- out.append(norm)
52
- return out
53
-
54
-
55
- def append_format_tokens(query: str, formats: List[str]) -> str:
56
- fmt_tokens: List[str] = []
57
- for ext in normalize_formats(formats):
58
- canon = FORMAT_TOKEN_MAP.get(ext, ext.upper())
59
- if canon not in fmt_tokens:
60
- fmt_tokens.append(canon)
61
-
62
- if not fmt_tokens:
63
- return query.strip()
64
- return (query.strip() + " " + " ".join(f"format:{t}" for t in fmt_tokens)).strip()
65
-
66
-
67
- def strip_legacy_original_formats_line(query: str) -> tuple[str, List[str]]:
68
- """Parse and remove legacy OriginalFormats: line from query text."""
69
- original_formats: List[str] = []
70
- clean_lines = []
71
-
72
- for line in (query or "").splitlines():
73
- if line.lower().startswith("originalformats:"):
74
- parts = line.split(":", 1)[1].strip().split()
75
- original_formats.extend(parts)
76
- continue
77
- clean_lines.append(line)
78
-
79
- base_query = " ".join(ln.strip() for ln in clean_lines if ln.strip())
80
- return base_query, normalize_formats(original_formats)
81
-
82
-
83
- def sanitize_retrieval_query(
84
- query: str,
85
- known_tool_names: List[str] | None = None,
86
- fallback_query: str | None = None,
87
- ) -> str:
88
- """
89
- Sanitize LLM-generated retrieval queries by removing repository drift terms.
90
-
91
- If the query collapses into a tool-name-only or low-signal query, fallback
92
- to the previous task-centric query when provided.
93
- """
94
- raw = (query or "").strip()
95
- if not raw:
96
- return (fallback_query or "").strip()
97
-
98
- # Remove URLs and punctuation-heavy fragments.
99
- s = re.sub(r"https?://\S+", " ", raw, flags=re.IGNORECASE)
100
- s = re.sub(r"www\.\S+", " ", s, flags=re.IGNORECASE)
101
-
102
- tokens = _tokenize_query(s)
103
- if not tokens:
104
- return (fallback_query or raw).strip()
105
-
106
- filtered = [t for t in tokens if t not in _REPO_DRIFT_TERMS]
107
- if not filtered:
108
- return (fallback_query or raw).strip()
109
-
110
- # Detect tool-name-only drift (e.g., "dhsegment official github repository").
111
- if known_tool_names:
112
- token_set = set(filtered)
113
- for nm in known_tool_names:
114
- nm_tokens = set(_tokenize_query(nm))
115
- if not nm_tokens:
116
- continue
117
- if token_set.issubset(nm_tokens) and len(token_set) <= 3:
118
- return (fallback_query or " ".join(filtered)).strip()
119
-
120
- low_signal = all(t in _LOW_SIGNAL_TERMS for t in filtered)
121
- if low_signal:
122
- return (fallback_query or " ".join(filtered)).strip()
123
-
124
- # If it became too short and a previous task query exists, prefer that.
125
- if len(filtered) <= 2 and fallback_query:
126
- return fallback_query.strip()
127
-
128
- return " ".join(filtered).strip()
 
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import List
5
+
6
+ FORMAT_TOKEN_MAP = {
7
+ "tif": "TIFF",
8
+ "tiff": "TIFF",
9
+ "nii": "NIfTI",
10
+ "nii.gz": "NIfTI",
11
+ "dcm": "DICOM",
12
+ "dicom": "DICOM",
13
+ "nrrd": "NRRD",
14
+ "png": "PNG",
15
+ "jpg": "JPEG",
16
+ "jpeg": "JPEG",
17
+ }
18
+
19
+ _REPO_DRIFT_TERMS = {
20
+ "github",
21
+ "repository",
22
+ "repo",
23
+ "official",
24
+ "readme",
25
+ "docs",
26
+ "documentation",
27
+ "source",
28
+ "sourcecode",
29
+ }
30
+
31
+ _LOW_SIGNAL_TERMS = _REPO_DRIFT_TERMS | {
32
+ "tool",
33
+ "tools",
34
+ "project",
35
+ "framework",
36
+ }
37
+
38
+
39
+ def _tokenize_query(query: str) -> List[str]:
40
+ return [t for t in re.findall(r"[a-z0-9_+-]+", (query or "").lower()) if t]
41
+
42
+
43
+ def normalize_formats(formats: List[str]) -> List[str]:
44
+ seen = set()
45
+ out: List[str] = []
46
+ for ext in formats:
47
+ norm = (ext or "").strip().lower()
48
+ if not norm or norm in seen:
49
+ continue
50
+ seen.add(norm)
51
+ out.append(norm)
52
+ return out
53
+
54
+
55
+ def append_format_tokens(query: str, formats: List[str]) -> str:
56
+ fmt_tokens: List[str] = []
57
+ for ext in normalize_formats(formats):
58
+ canon = FORMAT_TOKEN_MAP.get(ext, ext.upper())
59
+ if canon not in fmt_tokens:
60
+ fmt_tokens.append(canon)
61
+
62
+ if not fmt_tokens:
63
+ return query.strip()
64
+ return (query.strip() + " " + " ".join(f"format:{t}" for t in fmt_tokens)).strip()
65
+
66
+
67
+ def strip_legacy_original_formats_line(query: str) -> tuple[str, List[str]]:
68
+ """Parse and remove legacy OriginalFormats: line from query text."""
69
+ original_formats: List[str] = []
70
+ clean_lines = []
71
+
72
+ for line in (query or "").splitlines():
73
+ if line.lower().startswith("originalformats:"):
74
+ parts = line.split(":", 1)[1].strip().split()
75
+ original_formats.extend(parts)
76
+ continue
77
+ clean_lines.append(line)
78
+
79
+ base_query = " ".join(ln.strip() for ln in clean_lines if ln.strip())
80
+ return base_query, normalize_formats(original_formats)
81
+
82
+
83
+ def sanitize_retrieval_query(
84
+ query: str,
85
+ known_tool_names: List[str] | None = None,
86
+ fallback_query: str | None = None,
87
+ ) -> str:
88
+ """
89
+ Sanitize LLM-generated retrieval queries by removing repository drift terms.
90
+
91
+ If the query collapses into a tool-name-only or low-signal query, fallback
92
+ to the previous task-centric query when provided.
93
+ """
94
+ raw = (query or "").strip()
95
+ if not raw:
96
+ return (fallback_query or "").strip()
97
+
98
+ # Remove URLs and punctuation-heavy fragments.
99
+ s = re.sub(r"https?://\S+", " ", raw, flags=re.IGNORECASE)
100
+ s = re.sub(r"www\.\S+", " ", s, flags=re.IGNORECASE)
101
+
102
+ tokens = _tokenize_query(s)
103
+ if not tokens:
104
+ return (fallback_query or raw).strip()
105
+
106
+ filtered = [t for t in tokens if t not in _REPO_DRIFT_TERMS]
107
+ if not filtered:
108
+ return (fallback_query or raw).strip()
109
+
110
+ # Detect tool-name-only drift (e.g., "dhsegment official github repository").
111
+ if known_tool_names:
112
+ token_set = set(filtered)
113
+ for nm in known_tool_names:
114
+ nm_tokens = set(_tokenize_query(nm))
115
+ if not nm_tokens:
116
+ continue
117
+ if token_set.issubset(nm_tokens) and len(token_set) <= 3:
118
+ return (fallback_query or " ".join(filtered)).strip()
119
+
120
+ low_signal = all(t in _LOW_SIGNAL_TERMS for t in filtered)
121
+ if low_signal:
122
+ return (fallback_query or " ".join(filtered)).strip()
123
+
124
+ # If it became too short and a previous task query exists, prefer that.
125
+ if len(filtered) <= 2 and fallback_query:
126
+ return fallback_query.strip()
127
+
128
+ return " ".join(filtered).strip()
src/ai_agent/agent/tools/repo_info_tool.py CHANGED
@@ -2,7 +2,6 @@ from __future__ import annotations
2
 
3
  import asyncio
4
  import os
5
- import time
6
  from typing import Optional
7
  import repocards
8
 
@@ -10,6 +9,7 @@ from pydantic import BaseModel
10
 
11
  from .deepwiki_tool import get_wiki_contents, DeepWikiInput
12
  from .utils import _clip, get_catalog_docs, _is_github_url
 
13
 
14
  import logging
15
 
@@ -18,7 +18,9 @@ log = logging.getLogger("agent.repo_info")
18
  REPO_INFO_CACHE_TTL_SECONDS = int(os.getenv("REPO_INFO_CACHE_TTL_SECONDS", "3600"))
19
  REPO_INFO_CACHE_MAX_ENTRIES = int(os.getenv("REPO_INFO_CACHE_MAX_ENTRIES", "256"))
20
 
21
- _REPO_INFO_CACHE: dict[str, tuple[float, "RepoSummaryOutput"]] = {}
 
 
22
  _REPO_INFO_INFLIGHT: dict[str, asyncio.Future["RepoSummaryOutput"]] = {}
23
  _REPO_INFO_LOCK = asyncio.Lock()
24
 
@@ -41,25 +43,9 @@ def _normalize_cache_key(url: str) -> str:
41
  return url.strip().lower()
42
 
43
 
44
- def _evict_expired_entries(now: float) -> None:
45
- expired = [k for k, (exp, _) in _REPO_INFO_CACHE.items() if exp <= now]
46
- for key in expired:
47
- _REPO_INFO_CACHE.pop(key, None)
48
-
49
-
50
- def _enforce_cache_capacity() -> None:
51
- if len(_REPO_INFO_CACHE) <= REPO_INFO_CACHE_MAX_ENTRIES:
52
- return
53
- # Evict oldest expiration first. Good enough for bounded in-memory cache.
54
- keys_by_expiry = sorted(_REPO_INFO_CACHE.items(), key=lambda item: item[1][0])
55
- over = len(_REPO_INFO_CACHE) - REPO_INFO_CACHE_MAX_ENTRIES
56
- for key, _ in keys_by_expiry[:over]:
57
- _REPO_INFO_CACHE.pop(key, None)
58
-
59
-
60
  def _clear_repo_summary_cache_for_tests() -> None:
61
  """Test helper to avoid state leakage across test cases."""
62
- _REPO_INFO_CACHE.clear()
63
  _REPO_INFO_INFLIGHT.clear()
64
 
65
 
@@ -94,17 +80,31 @@ async def tool_repo_summary(input: RepoSummaryInput) -> RepoSummaryOutput:
94
  log.warning(f"Failed to lookup repo URL from catalog: {e}")
95
 
96
  cache_key = _normalize_cache_key(effective_url)
97
- now = time.monotonic()
98
 
 
 
 
 
 
 
 
 
 
 
 
99
  await _REPO_INFO_LOCK.acquire()
100
  try:
101
- _evict_expired_entries(now)
102
-
103
- cached_entry = _REPO_INFO_CACHE.get(cache_key)
104
- if cached_entry:
105
- _, cached = cached_entry
106
- log.info(f"Repo info cache hit for {effective_url}")
107
- return cached.model_copy(deep=True)
 
 
 
108
 
109
  inflight = _REPO_INFO_INFLIGHT.get(cache_key)
110
  if inflight is None:
@@ -133,19 +133,30 @@ async def tool_repo_summary(input: RepoSummaryInput) -> RepoSummaryOutput:
133
  _REPO_INFO_LOCK.release()
134
  raise
135
 
 
 
136
  await _REPO_INFO_LOCK.acquire()
137
  try:
138
- if result.source != "error" and REPO_INFO_CACHE_TTL_SECONDS > 0:
139
- expires_at = time.monotonic() + REPO_INFO_CACHE_TTL_SECONDS
140
- _REPO_INFO_CACHE[cache_key] = (expires_at, result)
141
- _enforce_cache_capacity()
142
-
143
  if not inflight.done():
144
  inflight.set_result(result)
145
  _REPO_INFO_INFLIGHT.pop(cache_key, None)
146
  finally:
147
  _REPO_INFO_LOCK.release()
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  return result.model_copy(deep=True)
150
 
151
 
 
2
 
3
  import asyncio
4
  import os
 
5
  from typing import Optional
6
  import repocards
7
 
 
9
 
10
  from .deepwiki_tool import get_wiki_contents, DeepWikiInput
11
  from .utils import _clip, get_catalog_docs, _is_github_url
12
+ from ai_agent.utils.cache_db import get_cache_db
13
 
14
  import logging
15
 
 
18
  REPO_INFO_CACHE_TTL_SECONDS = int(os.getenv("REPO_INFO_CACHE_TTL_SECONDS", "3600"))
19
  REPO_INFO_CACHE_MAX_ENTRIES = int(os.getenv("REPO_INFO_CACHE_MAX_ENTRIES", "256"))
20
 
21
+ _REPO_INFO_NS = "repo_info"
22
+
23
+ # In-flight request deduplication must remain in-memory (holds asyncio.Future objects)
24
  _REPO_INFO_INFLIGHT: dict[str, asyncio.Future["RepoSummaryOutput"]] = {}
25
  _REPO_INFO_LOCK = asyncio.Lock()
26
 
 
43
  return url.strip().lower()
44
 
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  def _clear_repo_summary_cache_for_tests() -> None:
47
  """Test helper to avoid state leakage across test cases."""
48
+ get_cache_db().clear(_REPO_INFO_NS)
49
  _REPO_INFO_INFLIGHT.clear()
50
 
51
 
 
80
  log.warning(f"Failed to lookup repo URL from catalog: {e}")
81
 
82
  cache_key = _normalize_cache_key(effective_url)
83
+ db = get_cache_db()
84
 
85
+ # --- Cache read outside the lock so we don't block other coroutines ---
86
+ try:
87
+ raw = await asyncio.to_thread(db.get, _REPO_INFO_NS, cache_key)
88
+ except Exception:
89
+ log.warning("Repo info cache read failed; treating as cache miss.", exc_info=True)
90
+ raw = None
91
+ if raw is not None:
92
+ log.info(f"Repo info cache hit for {effective_url}")
93
+ return RepoSummaryOutput.model_validate_json(raw)
94
+
95
+ # --- Lock only for inflight bookkeeping (pure in-memory, no I/O) ---
96
  await _REPO_INFO_LOCK.acquire()
97
  try:
98
+ # Re-check the cache inside the lock in case another coroutine wrote it
99
+ # between our lockless read above and acquiring the lock now.
100
+ try:
101
+ raw = await asyncio.to_thread(db.get, _REPO_INFO_NS, cache_key)
102
+ except Exception:
103
+ log.warning("Repo info cache read (after lock) failed; treating as cache miss.", exc_info=True)
104
+ raw = None
105
+ if raw is not None:
106
+ log.info(f"Repo info cache hit (after lock) for {effective_url}")
107
+ return RepoSummaryOutput.model_validate_json(raw)
108
 
109
  inflight = _REPO_INFO_INFLIGHT.get(cache_key)
110
  if inflight is None:
 
133
  _REPO_INFO_LOCK.release()
134
  raise
135
 
136
+ # --- Resolve the in-flight future immediately so waiters are unblocked
137
+ # before the (potentially slow) DB write happens. ---
138
  await _REPO_INFO_LOCK.acquire()
139
  try:
 
 
 
 
 
140
  if not inflight.done():
141
  inflight.set_result(result)
142
  _REPO_INFO_INFLIGHT.pop(cache_key, None)
143
  finally:
144
  _REPO_INFO_LOCK.release()
145
 
146
+ # --- DB write after waiters are already released ---
147
+ if result.source != "error" and REPO_INFO_CACHE_TTL_SECONDS > 0:
148
+ try:
149
+ await asyncio.to_thread(
150
+ db.set,
151
+ _REPO_INFO_NS,
152
+ cache_key,
153
+ result.model_dump_json(),
154
+ ttl_seconds=REPO_INFO_CACHE_TTL_SECONDS,
155
+ max_entries=REPO_INFO_CACHE_MAX_ENTRIES,
156
+ )
157
+ except Exception:
158
+ log.warning("Repo info cache write failed; result will not be cached.", exc_info=True)
159
+
160
  return result.model_copy(deep=True)
161
 
162
 
src/ai_agent/agent/tools/search_alternative_tool.py CHANGED
@@ -1,73 +1,73 @@
1
- from __future__ import annotations
2
-
3
- from typing import List
4
- from pydantic import BaseModel, Field
5
-
6
- from ai_agent.generator.schema import CandidateDoc
7
- from .utils import get_known_names, get_pipeline
8
- from .query_utils import append_format_tokens, normalize_formats, sanitize_retrieval_query
9
-
10
-
11
- class SearchAlternativeInput(BaseModel):
12
- """
13
- Input for searching with an alternative query formulation.
14
-
15
- Use this when initial search results are insufficient and you want to
16
- try a different phrasing or broader/narrower terms.
17
- """
18
-
19
- alternative_query: str = Field(
20
- description="Alternative query phrasing to try (can be similar terms, broader/narrower, etc.)"
21
- )
22
- excluded: List[str] = Field(default_factory=list)
23
- top_k: int = 12
24
- original_formats: List[str] = Field(default_factory=list)
25
- image_paths: List[str] = Field(default_factory=list)
26
-
27
-
28
- class SearchAlternativeOutput(BaseModel):
29
- candidates: List[CandidateDoc]
30
- query_used: str
31
-
32
-
33
- def tool_search_alternative(inp: SearchAlternativeInput) -> SearchAlternativeOutput:
34
- """
35
- Search with an alternative query formulation, with automatic reranking.
36
-
37
- This tool allows the agent to explicitly try a different search approach
38
- when initial results are not satisfactory.
39
- """
40
- pipe = get_pipeline()
41
-
42
- # Use the alternative query directly
43
- query = sanitize_retrieval_query(
44
- inp.alternative_query.strip(), known_tool_names=get_known_names()
45
- )
46
-
47
- # Normalize formats
48
- original_formats: List[str] = normalize_formats(inp.original_formats)
49
- query = append_format_tokens(query, original_formats)
50
-
51
- # Call retrieve() which includes automatic reranking
52
- hits = pipe.retrieve(
53
- query,
54
- image_paths=inp.image_paths or None,
55
- exclusions=inp.excluded,
56
- top_k=inp.top_k,
57
- )
58
-
59
- # Convert hits to CandidateDoc objects
60
- candidates: List[CandidateDoc] = []
61
- for h in hits:
62
- d = h.get("doc")
63
- if not d:
64
- continue
65
- try:
66
- candidates.append(CandidateDoc.model_validate(d.model_dump(mode="python")))
67
- except Exception:
68
- continue
69
-
70
- return SearchAlternativeOutput(
71
- candidates=candidates,
72
- query_used=query,
73
- )
 
1
+ from __future__ import annotations
2
+
3
+ from typing import List
4
+ from pydantic import BaseModel, Field
5
+
6
+ from ai_agent.generator.schema import CandidateDoc
7
+ from .utils import get_known_names, get_pipeline
8
+ from .query_utils import append_format_tokens, normalize_formats, sanitize_retrieval_query
9
+
10
+
11
+ class SearchAlternativeInput(BaseModel):
12
+ """
13
+ Input for searching with an alternative query formulation.
14
+
15
+ Use this when initial search results are insufficient and you want to
16
+ try a different phrasing or broader/narrower terms.
17
+ """
18
+
19
+ alternative_query: str = Field(
20
+ description="Alternative query phrasing to try (can be similar terms, broader/narrower, etc.)"
21
+ )
22
+ excluded: List[str] = Field(default_factory=list)
23
+ top_k: int = 12
24
+ original_formats: List[str] = Field(default_factory=list)
25
+ image_paths: List[str] = Field(default_factory=list)
26
+
27
+
28
+ class SearchAlternativeOutput(BaseModel):
29
+ candidates: List[CandidateDoc]
30
+ query_used: str
31
+
32
+
33
+ def tool_search_alternative(inp: SearchAlternativeInput) -> SearchAlternativeOutput:
34
+ """
35
+ Search with an alternative query formulation, with automatic reranking.
36
+
37
+ This tool allows the agent to explicitly try a different search approach
38
+ when initial results are not satisfactory.
39
+ """
40
+ pipe = get_pipeline()
41
+
42
+ # Use the alternative query directly
43
+ query = sanitize_retrieval_query(
44
+ inp.alternative_query.strip(), known_tool_names=get_known_names()
45
+ )
46
+
47
+ # Normalize formats
48
+ original_formats: List[str] = normalize_formats(inp.original_formats)
49
+ query = append_format_tokens(query, original_formats)
50
+
51
+ # Call retrieve() which includes automatic reranking
52
+ hits = pipe.retrieve(
53
+ query,
54
+ image_paths=inp.image_paths or None,
55
+ exclusions=inp.excluded,
56
+ top_k=inp.top_k,
57
+ )
58
+
59
+ # Convert hits to CandidateDoc objects
60
+ candidates: List[CandidateDoc] = []
61
+ for h in hits:
62
+ d = h.get("doc")
63
+ if not d:
64
+ continue
65
+ try:
66
+ candidates.append(CandidateDoc.model_validate(d.model_dump(mode="python")))
67
+ except Exception:
68
+ continue
69
+
70
+ return SearchAlternativeOutput(
71
+ candidates=candidates,
72
+ query_used=query,
73
+ )
src/ai_agent/agent/tools/sparql_tool.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SPARQL query tool — lets the agent ask the catalog's GraphDB anything the
2
+ RAG can't answer cleanly (counts, distincts, structural filters, etc.).
3
+
4
+ Safety:
5
+ · read-only by syntax check — UPDATE / INSERT / DELETE / DROP / CLEAR /
6
+ CREATE / LOAD / COPY / MOVE / ADD are rejected before the query goes
7
+ anywhere near the endpoint
8
+ · result row count is capped (we inject / override LIMIT)
9
+ · network and query time are bounded (SPARQLWrapper timeout)
10
+ · auth comes from the same GRAPHDB_USER / GRAPHDB_PASSWORD env the
11
+ catalog sync uses
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ import os
18
+ import re
19
+ from typing import Any, Dict, List, Optional
20
+
21
+ from pydantic import BaseModel, Field
22
+ from SPARQLWrapper import JSON, SPARQLWrapper
23
+
24
+ log = logging.getLogger("agent.tools.sparql")
25
+
26
+ # Default ceiling for any SELECT — overrideable via input.
27
+ DEFAULT_LIMIT = 50
28
+ HARD_MAX_LIMIT = 200
29
+
30
+ # Update keywords that must NOT appear at the top level of the query. We
31
+ # match on word boundaries (case-insensitive) outside of `<...>` IRIs and
32
+ # quoted strings — a real parser would be safer but for an agent-authored
33
+ # query this catches the common cases.
34
+ _UPDATE_KEYWORDS = re.compile(
35
+ r"\b("
36
+ r"INSERT|DELETE|UPDATE|DROP|CLEAR|CREATE|LOAD|COPY|MOVE|ADD|MODIFY"
37
+ r")\b",
38
+ re.IGNORECASE,
39
+ )
40
+
41
+ _LIMIT_RE = re.compile(r"\blimit\s+(\d+)\b", re.IGNORECASE)
42
+
43
+
44
+ class SparqlQueryInput(BaseModel):
45
+ query: str = Field(..., description="A SPARQL SELECT / ASK query.")
46
+ limit: int = Field(
47
+ default=DEFAULT_LIMIT,
48
+ ge=1,
49
+ le=HARD_MAX_LIMIT,
50
+ description="Maximum row count (we'll add / cap LIMIT in the query).",
51
+ )
52
+
53
+
54
+ class SparqlQueryOutput(BaseModel):
55
+ columns: List[str] = Field(default_factory=list)
56
+ rows: List[Dict[str, Any]] = Field(default_factory=list)
57
+ row_count: int = 0
58
+ truncated: bool = False
59
+ boolean: Optional[bool] = None # only for ASK queries
60
+ query_executed: str = ""
61
+ error: Optional[str] = None
62
+
63
+
64
+ def _strip_strings_and_iris(q: str) -> str:
65
+ """Remove `"..."`, `'...'`, and `<...>` so keyword scanning isn't fooled
66
+ by literal text inside the query."""
67
+ q = re.sub(r'"(?:[^"\\]|\\.)*"', '""', q)
68
+ q = re.sub(r"'(?:[^'\\]|\\.)*'", "''", q)
69
+ q = re.sub(r"<[^>]*>", "<>", q)
70
+ return q
71
+
72
+
73
+ def _looks_readonly(query: str) -> bool:
74
+ scrubbed = _strip_strings_and_iris(query)
75
+ return _UPDATE_KEYWORDS.search(scrubbed) is None
76
+
77
+
78
+ def _enforce_limit(query: str, limit: int) -> str:
79
+ """Inject LIMIT, or cap an existing one to `limit`. SELECT only — ASK,
80
+ CONSTRUCT, DESCRIBE pass through unchanged (LIMIT is moot for ASK; the
81
+ other two are blocked upstream)."""
82
+ if not re.search(r"\bselect\b", query, re.IGNORECASE):
83
+ return query
84
+ m = _LIMIT_RE.search(query)
85
+ if m:
86
+ requested = int(m.group(1))
87
+ if requested <= limit:
88
+ return query
89
+ return _LIMIT_RE.sub(f"LIMIT {limit}", query)
90
+ trimmed = query.rstrip(" \t\n;")
91
+ return trimmed + "\nLIMIT " + str(limit)
92
+
93
+
94
+ def tool_sparql_query(inp: SparqlQueryInput) -> SparqlQueryOutput:
95
+ """Execute a SPARQL SELECT or ASK against GRAPHDB_URL and return a flat
96
+ table of bindings."""
97
+ endpoint = (os.getenv("GRAPHDB_URL") or "").strip()
98
+ if not endpoint:
99
+ return SparqlQueryOutput(
100
+ error="GRAPHDB_URL is not configured on the server.",
101
+ query_executed=inp.query,
102
+ )
103
+
104
+ if not _looks_readonly(inp.query):
105
+ return SparqlQueryOutput(
106
+ error="rejected_update_query — only read-only SPARQL is allowed",
107
+ query_executed=inp.query,
108
+ )
109
+
110
+ # We currently only normalise SELECT/ASK. CONSTRUCT/DESCRIBE return RDF
111
+ # graphs which would need a different output schema — bail with a clear
112
+ # message rather than silently misformatting.
113
+ qkind = re.search(
114
+ r"\b(select|ask|construct|describe)\b", inp.query, re.IGNORECASE
115
+ )
116
+ if not qkind:
117
+ return SparqlQueryOutput(
118
+ error="missing_query_form — start the query with SELECT or ASK",
119
+ query_executed=inp.query,
120
+ )
121
+ form = qkind.group(1).lower()
122
+ if form in ("construct", "describe"):
123
+ return SparqlQueryOutput(
124
+ error=f"unsupported_form — {form.upper()} not supported by this tool yet; rephrase as SELECT",
125
+ query_executed=inp.query,
126
+ )
127
+
128
+ final_query = _enforce_limit(inp.query, inp.limit) if form == "select" else inp.query
129
+
130
+ sparql = SPARQLWrapper(endpoint)
131
+ sparql.setQuery(final_query)
132
+ sparql.setReturnFormat(JSON)
133
+ user = os.getenv("GRAPHDB_USER")
134
+ pwd = os.getenv("GRAPHDB_PASSWORD")
135
+ if user and pwd:
136
+ sparql.setCredentials(user=user, passwd=pwd)
137
+ sparql.setTimeout(20)
138
+
139
+ try:
140
+ result = sparql.query().convert()
141
+ except Exception as exc:
142
+ log.warning("sparql_query failed: %s", exc)
143
+ return SparqlQueryOutput(
144
+ error=f"sparql_error: {exc}", query_executed=final_query
145
+ )
146
+
147
+ # ASK → boolean
148
+ if "boolean" in result:
149
+ return SparqlQueryOutput(
150
+ boolean=bool(result["boolean"]),
151
+ row_count=0,
152
+ query_executed=final_query,
153
+ )
154
+
155
+ cols: List[str] = list(result.get("head", {}).get("vars", []))
156
+ bindings = result.get("results", {}).get("bindings", []) or []
157
+ rows: List[Dict[str, Any]] = []
158
+ for b in bindings:
159
+ row: Dict[str, Any] = {}
160
+ for c in cols:
161
+ cell = b.get(c)
162
+ if cell is None:
163
+ row[c] = None
164
+ else:
165
+ row[c] = cell.get("value")
166
+ rows.append(row)
167
+
168
+ truncated = len(rows) >= inp.limit
169
+ return SparqlQueryOutput(
170
+ columns=cols,
171
+ rows=rows,
172
+ row_count=len(rows),
173
+ truncated=truncated,
174
+ query_executed=final_query,
175
+ )
176
+
177
+
178
+ __all__ = ["SparqlQueryInput", "SparqlQueryOutput", "tool_sparql_query"]
src/ai_agent/api/deps.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared FastAPI dependencies.
2
+
3
+ - ``require_auth``: simple cookie-based gate using ``APP_PASSWORD`` env var.
4
+ If ``APP_PASSWORD`` is empty/unset, auth is disabled entirely (dev mode).
5
+ - ``get_pipeline``: returns the singleton ``RAGImagingPipeline`` (lazy-init,
6
+ shared with the Gradio path via ``ai_agent.core.pipeline_registry``).
7
+ - ``get_doc_index``: name -> SoftwareDoc, derived from the pipeline.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import hmac
14
+ import logging
15
+ import os
16
+ from typing import Dict, Optional
17
+
18
+ from fastapi import Cookie, Depends, HTTPException, status
19
+
20
+ from ai_agent.api.pipeline import RAGImagingPipeline
21
+ from ai_agent.core.pipeline_registry import get_pipeline as _shared_pipeline
22
+ from ai_agent.retriever.software_doc import SoftwareDoc
23
+
24
+ log = logging.getLogger("api.deps")
25
+
26
+ AUTH_COOKIE_NAME = "ai_agent_auth"
27
+ _AUTH_VERSION = "v1"
28
+
29
+
30
+ def _expected_cookie() -> Optional[str]:
31
+ """The cookie value clients must present, or None if auth is disabled."""
32
+ pw = os.getenv("APP_PASSWORD") or ""
33
+ if not pw:
34
+ return None
35
+ return hmac.new(
36
+ pw.encode("utf-8"),
37
+ _AUTH_VERSION.encode("utf-8"),
38
+ hashlib.sha256,
39
+ ).hexdigest()
40
+
41
+
42
+ def make_cookie_value(password: str) -> str:
43
+ """Compute the cookie value for a successful login."""
44
+ return hmac.new(
45
+ password.encode("utf-8"),
46
+ _AUTH_VERSION.encode("utf-8"),
47
+ hashlib.sha256,
48
+ ).hexdigest()
49
+
50
+
51
+ def verify_password(password: str) -> bool:
52
+ expected = os.getenv("APP_PASSWORD") or ""
53
+ if not expected:
54
+ # Auth disabled — accept any password (login is then effectively a noop).
55
+ return True
56
+ return hmac.compare_digest(password, expected)
57
+
58
+
59
+ def auth_disabled() -> bool:
60
+ return not (os.getenv("APP_PASSWORD") or "")
61
+
62
+
63
+ async def require_auth(
64
+ ai_agent_auth: Optional[str] = Cookie(default=None, alias=AUTH_COOKIE_NAME),
65
+ ) -> None:
66
+ """FastAPI dependency: 401 if cookie missing or wrong (when auth on)."""
67
+ expected = _expected_cookie()
68
+ if expected is None:
69
+ return # auth disabled
70
+ if not ai_agent_auth or not hmac.compare_digest(ai_agent_auth, expected):
71
+ raise HTTPException(
72
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="not_authenticated"
73
+ )
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # Pipeline / doc index providers (re-export so routers don't import internals).
78
+ # ---------------------------------------------------------------------------
79
+ def get_pipeline() -> RAGImagingPipeline:
80
+ index_dir = os.getenv("RAG_INDEX_DIR", "artifacts/rag_index")
81
+ return _shared_pipeline(index_dir=index_dir)
82
+
83
+
84
+ def get_doc_index(
85
+ pipe: RAGImagingPipeline = Depends(get_pipeline),
86
+ ) -> Dict[str, SoftwareDoc]:
87
+ try:
88
+ docs = list(pipe.index.docs.values())
89
+ return {d.name: d for d in docs if getattr(d, "name", None)}
90
+ except Exception:
91
+ log.exception("Failed to build doc_index from pipeline")
92
+ return {}
93
+
94
+
95
+ __all__ = [
96
+ "AUTH_COOKIE_NAME",
97
+ "auth_disabled",
98
+ "get_doc_index",
99
+ "get_pipeline",
100
+ "make_cookie_value",
101
+ "require_auth",
102
+ "verify_password",
103
+ ]