Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
name: tool-microservice
name: modai-tool
description: How to create a new tool microservice for modAI-chat. Tools are independent HTTP microservices that expose an OpenAPI spec and a trigger endpoint. They are registered in modAI's tool registry via config.yaml.
---

Expand Down Expand Up @@ -53,7 +53,7 @@ Start the service and check that `/openapi.json` contains:

- `operationId` — unique name for the tool (e.g. `"roll_dice"`)
- `summary` or `description` — what the tool does (shown to the LLM)
- `requestBody.content.application/json.schema` — input parameters
- `requestBody.content.application/json.schema` — input parameters (optional if all inputs come from path/header)

```bash
curl http://localhost:8001/openapi.json | jq '.paths'
Expand Down Expand Up @@ -82,26 +82,103 @@ The dice roller produces this structure:
}
```

#### Path Parameters

If the trigger URL contains path variables (e.g. `/users/{user_id}/orders/{order_id}`), declare them as `"in": "path"` in the `parameters` array. The registry merges them into the tool's parameter schema so the LLM knows to supply them. At invocation time, modAI substitutes their values directly into the URL — they are **not** sent in the request body.

```json
{
"/users/{user_id}/orders/{order_id}": {
"get": {
"summary": "Get a user order",
"operationId": "get_user_order",
"parameters": [
{
"name": "user_id",
"in": "path",
"required": true,
"description": "The user's ID",
"schema": { "type": "string" }
},
{
"name": "order_id",
"in": "path",
"required": true,
"description": "The order's ID",
"schema": { "type": "integer" }
}
]
}
}
}
```

#### Header Parameters

Parameters your tool expects as **HTTP request headers** (e.g. `X-Session-Id`) must be declared as `"in": "header"` in the `parameters` array. The registry includes them in the tool's parameter schema; at invocation time modAI forwards their values as HTTP headers — they are **not** sent in the request body.

```json
{
"/data": {
"get": {
"summary": "Fetch session data",
"operationId": "fetch_data",
"parameters": [
{
"name": "X-Session-Id",
"in": "header",
"required": true,
"description": "Active session identifier",
"schema": { "type": "string" }
}
]
}
}
}
```

### 3. Register in modAI config.yaml

Add the tool to the `tool_registry` module's `tools` list in `config.yaml` (and `default_config.yaml` if it should be a default):
Add the tool to the `openapi_tool_registry` module's `tools` list in `config.yaml` (and `default_config.yaml` if it should be a default):

```yaml
modules:
tool_registry:
class: modai.modules.tools.tool_registry.HttpToolRegistryModule
openapi_tool_registry:
class: modai.modules.tools.tool_registry_openapi.OpenAPIToolRegistryModule
module_dependencies:
http_client: "http_client"
config:
tools:
- url: http://localhost:8001/roll
method: POST
```

Each entry has:
- **`url`**: The full trigger endpoint URL (not the base URL)
- **`url`**: The full trigger endpoint URL (including any path-parameter placeholders, e.g. `http://svc:8000/users/{user_id}/orders/{order_id}`)
- **`method`**: The HTTP method to invoke the tool (POST, PUT, GET, etc.)

The registry derives the base URL from `url` (strips the path) and appends `/openapi.json` to fetch the spec.

#### Hiding known variables with PredefinedVariablesToolRegistryModule

The default `tool_registry` in `config.yaml` is a `PredefinedVariablesToolRegistryModule` that wraps the OpenAPI registry. When the caller already has a value for a tool parameter (e.g. a session ID that comes from the auth headers), that parameter can be hidden from the LLM's tool definition so the LLM is never asked to supply it.

- **Direct match**: if a tool has a body/path parameter named `session_id` and the predefined params dict contains `_session_id`, `session_id` is stripped automatically.
- **Configured mapping**: if a tool uses a header parameter named `X-Session-Id` (which differs from the predefined variable name `session_id`), add a `variable_mappings` entry:

```yaml
modules:
tool_registry:
class: modai.modules.tools.tool_registry_predefined_vars.PredefinedVariablesToolRegistryModule
module_dependencies:
delegate_registry: "openapi_tool_registry"
config:
variable_mappings:
X-Session-Id: session_id # _session_id predefined value → X-Session-Id header
```

At invocation time, modAI translates `_session_id` back to `X-Session-Id` and forwards it as an HTTP header — the LLM never sees it.

### 4. Test the Integration

1. Start the tool microservice
Expand Down Expand Up @@ -133,14 +210,19 @@ Expected:
| OpenAPI spec location | `/openapi.json` at service root |
| Tool name | `operationId` from the OpenAPI spec |
| Tool description | `summary` (preferred) or `description` from the operation |
| Parameters | `requestBody.content.application/json.schema` |
| Body parameters | `requestBody.content.application/json.schema` |
| Path parameters | `"in": "path"` in `parameters` array — substituted into the URL at invocation |
| Header parameters | `"in": "header"` in `parameters` array — forwarded as HTTP headers at invocation |
| HTTP method | Choose what's idiomatic (POST for actions, GET for queries, etc.) |
| Error handling | Return appropriate HTTP status codes; modAI logs warnings for unreachable tools |

## Common Pitfalls

- **Missing `operationId`**: The tool will be silently skipped. Always set `operationId` on your trigger operation.
- **Wrong URL in config**: The `url` must be the full trigger endpoint (e.g. `http://localhost:8001/roll`), not just the base URL. The registry strips the path to derive the base for fetching `/openapi.json`.
- **Path variables in URL but not in spec**: If the configured `url` contains `{param}` placeholders, the corresponding `"in": "path"` parameters must be declared in the spec. Otherwise the LLM won't know to supply them and the URL won't be substituted correctly.
- **Header params missing from `parameters` array**: Header parameters must be declared with `"in": "header"` in the spec — they are not inferred from the request body schema. Undeclared header params will never be forwarded.
- **Header param name mismatch with predefined variables**: If your header param is named `X-Session-Id` but the predefined variable is `_session_id`, the value won't be injected automatically. Add a `variable_mappings` entry in the `tool_registry` config to bridge the naming difference.
- **Multiple operations**: The registry uses the **first** operation with an `operationId` it finds. Keep one trigger operation per tool service.
- **Non-JSON responses**: The LLM expects JSON results. Always return `application/json`.

Expand Down
25 changes: 24 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,36 @@
version: 2
updates:
- package-ecosystem: uv
directory: '/'
directories:
- '/backend/omni'
- '/backend/tools/dice-roller'
schedule:
interval: weekly
target-branch: 'main'
groups:
all-uv:
patterns:
- "*"

- package-ecosystem: npm
directories:
- '/frontend_omni'
- '/e2e_tests/tests_omni_full'
- '/e2e_tests/tests_omni_light'
schedule:
interval: weekly
target-branch: 'main'
groups:
all-pnpm:
patterns:
- "*"

- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: weekly
target-branch: 'main'
groups:
all-gha:
patterns:
- "*"
3 changes: 3 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Workspace root – prek discovers nested project configs automatically.
# See https://prek.j178.dev/workspace/
repos: []
30 changes: 30 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,36 @@ pnpm check # Run linter

For comprehensive e2e testing best practices and patterns, refer to `e2e_tests/BEST_PRACTICES.md`.

## Git Hooks (prek)

Pre-commit hooks use prek's **workspace mode**. Each sub-project has its own
`.pre-commit-config.yaml` (with `orphan: true`), discovered automatically from
the root `.pre-commit-config.yaml`.

Layout:
- `.pre-commit-config.yaml` — workspace root (empty, enables discovery)
- `backend/omni/.pre-commit-config.yaml` — ruff format + ruff check
- `backend/tools/dice-roller/.pre-commit-config.yaml` — ruff format + ruff check
- `frontend_omni/.pre-commit-config.yaml` — biome check
- `e2e_tests/tests_omni_full/.pre-commit-config.yaml` — biome check
- `e2e_tests/tests_omni_light/.pre-commit-config.yaml` — biome check

Hooks check (but do not auto-fix) on every commit and fail if issues remain.

**One-time setup** after cloning:
```bash
uv tool install prek # Install prek binary (skip if already installed)
prek install # Wire hooks into .git/hooks/pre-commit
```

**Manual run:**
```bash
prek run # Run on staged files only
prek run --all-files # Run on all files
prek run backend/omni/ # Run hooks for a specific project
prek run ruff-check # Run a single hook by id
```

## Development Workflow

1. **Read Architecture**: Always read relevant architecture docs first
Expand Down
18 changes: 18 additions & 0 deletions backend/omni/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
orphan: true

repos:
- repo: local
hooks:
- id: ruff-format
name: ruff format check
language: system
entry: uv run ruff format --check src
always_run: true
pass_filenames: false

- id: ruff-check
name: ruff check
language: system
entry: uv run ruff check src
always_run: true
pass_filenames: false
20 changes: 18 additions & 2 deletions backend/omni/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,13 @@ modules:
session: "session"
user_settings_store: "user_settings_store"

tool_registry:
class: modai.modules.tools.tool_registry.HttpToolRegistryModule
http_client:
class: modai.modules.http_client.httpx_http_client_module.HttpxHttpClientModule

openapi_tool_registry:
class: modai.modules.tools.tool_registry_openapi.OpenAPIToolRegistryModule
module_dependencies:
http_client: "http_client"
config:
tools:
- url: http://localhost:8001/roll
Expand All @@ -75,6 +80,17 @@ modules:
# - url: http://web-search-service:8000/search
# method: PUT

tool_registry:
class: modai.modules.tools.tool_registry_predefined_vars.PredefinedVariablesToolRegistryModule
module_dependencies:
delegate_registry: "openapi_tool_registry"
config:
# Map tool parameter names that differ from the predefined variable name.
# Format: <tool_param_name>: <predefined_var_name_without_leading_underscore>
# Example: the predefined _session_id fills the tool's X-Session-Id header param.
# variable_mappings:
# X-Session-Id: session_id

tools_web:
class: modai.modules.tools.tools_web_module.OpenAIToolsWebModule
module_dependencies:
Expand Down
Loading