mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-14 02:09:22 -04:00
refactor: final Agent loader
This commit is contained in:
@@ -216,6 +216,7 @@ For optimal coding assistance, we recommend using these top-tier models:
|
|||||||
|
|
||||||
### Additional Configuration
|
### Additional Configuration
|
||||||
|
|
||||||
|
- **[Creating and Extending Agents](docs/creating-agents.md)** - Add custom agents or override bundled ones with TOML profiles
|
||||||
- **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts
|
- **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts
|
||||||
- **[Chat Summarization](docs/chat-summarization.md)** - Compress conversations to save context tokens
|
- **[Chat Summarization](docs/chat-summarization.md)** - Compress conversations to save context tokens
|
||||||
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
|
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
|
||||||
@@ -531,7 +532,7 @@ If you find QodeAssist helpful, there are several ways you can support the proje
|
|||||||
|
|
||||||
1. **Report Issues**: If you encounter any bugs or have suggestions for improvements, please [open an issue](https://github.com/Palm1r/qodeassist/issues) on our GitHub repository.
|
1. **Report Issues**: If you encounter any bugs or have suggestions for improvements, please [open an issue](https://github.com/Palm1r/qodeassist/issues) on our GitHub repository.
|
||||||
|
|
||||||
2. **Contribute**: Feel free to submit pull requests with bug fixes or new features.
|
2. **Contribute**: Feel free to submit pull requests with bug fixes or new features. The easiest contribution is an agent preset for a provider or model you use — it's a single TOML file, no C++ required; see [Contributing your agent](docs/creating-agents.md#contributing-your-agent-to-qodeassist).
|
||||||
|
|
||||||
3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers.
|
3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers.
|
||||||
|
|
||||||
@@ -579,6 +580,10 @@ cmake --build .
|
|||||||
|
|
||||||
## For Contributors
|
## For Contributors
|
||||||
|
|
||||||
|
### Adding an agent preset
|
||||||
|
|
||||||
|
New provider/model presets are plain TOML — extend a provider base, register the file in `agents.qrc`, and the test suite validates it automatically. Step-by-step guide: [docs/creating-agents.md](docs/creating-agents.md#contributing-your-agent-to-qodeassist).
|
||||||
|
|
||||||
### Code Style
|
### Code Style
|
||||||
|
|
||||||
- **QML**: Follow [QML Coding Guide](https://github.com/Furkanzmc/QML-Coding-Guide) by @Furkanzmc
|
- **QML**: Follow [QML Coding Guide](https://github.com/Furkanzmc/QML-Coding-Guide) by @Furkanzmc
|
||||||
|
|||||||
@@ -79,22 +79,31 @@ A missing/typo'd partial is a **load-time** error.
|
|||||||
### 3. `extends` shares config down a hierarchy
|
### 3. `extends` shares config down a hierarchy
|
||||||
|
|
||||||
`extends` already exists (`resolveExtends` + `deepMerge` + `abstract`/`hidden`); it
|
`extends` already exists (`resolveExtends` + `deepMerge` + `abstract`/`hidden`); it
|
||||||
keeps doing what it does, now over the structured `[body]` too. A flat 2 levels — each
|
keeps doing what it does, now over the structured `[body]` too. Each API-shape base
|
||||||
provider base sets `system_prompt = """{{ agent_role("developer") }}"""` (the role text
|
sets `system_prompt = """{{ agent_role() }}"""` (the role text comes from the role
|
||||||
comes from the role JSON via the `agent_role` callback; see below). No shared root base:
|
JSON via the `agent_role` callback; see below). No shared root base. Between the
|
||||||
|
API-shape base and the concrete agents sits one thin abstract base **per provider**
|
||||||
|
(provider_instance + endpoint only) — the designated extension point for user
|
||||||
|
agents, so a custom agent is `extends` + `name` + `model`:
|
||||||
|
|
||||||
```
|
```
|
||||||
openai_base (abstract) → system_prompt + provider/endpoint/enable_tools + [body]
|
openai_base (abstract) → system_prompt + [body] (API shape)
|
||||||
├─ openai_chat → name
|
├─ mistral_base (abstract) → provider, endpoint (per-provider)
|
||||||
├─ mistral_chat → name, provider, endpoint
|
│ ├─ mistral_chat → name, model
|
||||||
└─ mistral_reasoning → + enable_thinking
|
│ └─ mistral_reasoning → name, model + enable_thinking
|
||||||
anthropic_base (abstract) → system_prompt + provider/endpoint + [body]
|
├─ openrouter_base (abstract) ...
|
||||||
├─ claude_chat → name
|
└─ openai_chat → name, model (own provider = no mid layer)
|
||||||
└─ claude_sonnet → + [body] thinking / output_config
|
anthropic_base (abstract) → system_prompt + provider/endpoint + [body]
|
||||||
google_base (abstract) → system_prompt + provider + [body]
|
└─ claude_sonnet46 → name, model + [body] thinking / output_config
|
||||||
└─ gemini_chat → endpoint (${MODEL}) + [body.generationConfig] thinkingConfig
|
google_base (abstract) → system_prompt + provider + [body]
|
||||||
|
└─ gemini_chat → endpoint (${MODEL}) + [body.generationConfig] thinkingConfig
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Bundled agents are read-only: the loader rejects a user file that reuses a bundled
|
||||||
|
`name`. Customisation = a user agent under a new name extending a bundled base (or a
|
||||||
|
concrete bundled agent); the per-agent model override in settings covers the
|
||||||
|
model-only case without any file.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- `[body]` is shared whole when identical (the 8 OpenAI-compatible providers); a
|
- `[body]` is shared whole when identical (the 8 OpenAI-compatible providers); a
|
||||||
variant overrides only the differing field — no duplicated body.
|
variant overrides only the differing field — no duplicated body.
|
||||||
|
|||||||
@@ -161,8 +161,13 @@ string values render and splice as raw JSON, literals pass through, empty render
|
|||||||
drop the key). `include` resolves only sandboxed partial roots. Profiles validate
|
drop the key). `include` resolves only sandboxed partial roots. Profiles validate
|
||||||
at load: a referenced partial must resolve and the assembled body must parse as
|
at load: a referenced partial must resolve and the assembled body must parse as
|
||||||
JSON against a synthetic context — config errors surface in the agents settings
|
JSON against a synthetic context — config errors surface in the agents settings
|
||||||
page, never as a silent runtime drop. Full spec:
|
page, never as a silent runtime drop. The loader also lints: unknown top-level /
|
||||||
[`agent-templates-design.md`](agent-templates-design.md).
|
`[match]` keys and same-layer duplicate names are warnings; a user file that
|
||||||
|
reuses a bundled agent's name is rejected (bundled agents cannot be replaced —
|
||||||
|
users extend them, or the per-provider abstract bases, under a new name);
|
||||||
|
`abstract` and `hidden` are never inherited through `extends`. Full spec:
|
||||||
|
[`agent-templates-design.md`](agent-templates-design.md); user-facing guide:
|
||||||
|
[`creating-agents.md`](creating-agents.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
339
docs/creating-agents.md
Normal file
339
docs/creating-agents.md
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
# Creating and Extending Agents
|
||||||
|
|
||||||
|
An *agent* is a TOML profile that tells QodeAssist which provider to call,
|
||||||
|
which model to use, and exactly what request body to send. All bundled agents
|
||||||
|
(Settings → QodeAssist → Agents) are built from the same files described here —
|
||||||
|
anything a bundled agent does, a user agent can do too.
|
||||||
|
|
||||||
|
## Where user agents live
|
||||||
|
|
||||||
|
Drop `*.toml` files into the user agents directory:
|
||||||
|
|
||||||
|
| OS | Path |
|
||||||
|
|---|---|
|
||||||
|
| Linux / macOS | `~/.config/QtProject/qtcreator/qodeassist/config/agents/` |
|
||||||
|
| Windows | `%APPDATA%\QtProject\qtcreator\qodeassist\config\agents\` |
|
||||||
|
|
||||||
|
QodeAssist creates the directory on startup. Files are loaded at plugin
|
||||||
|
startup; after adding or editing a file, restart Qt Creator.
|
||||||
|
|
||||||
|
Two layers are loaded:
|
||||||
|
|
||||||
|
1. **Bundled** agents shipped inside the plugin — read-only.
|
||||||
|
2. **User** agents from the directory above (marked with a `user` pill).
|
||||||
|
|
||||||
|
Agent `name`s are global across both layers. A user file that reuses a
|
||||||
|
bundled agent's `name` is rejected with an error — bundled agents cannot be
|
||||||
|
replaced; create your own agent under a new name and `extends` what you want
|
||||||
|
to build on. Two *user* files with the same `name` produce a warning, and
|
||||||
|
the alphabetically later file wins.
|
||||||
|
|
||||||
|
Load errors and warnings (TOML syntax, unknown keys, missing `extends`
|
||||||
|
parents, bodies that don't render to valid JSON) are reported in Qt Creator's
|
||||||
|
**General Messages** pane, prefixed with `[Agents]`.
|
||||||
|
|
||||||
|
## Minimal example
|
||||||
|
|
||||||
|
```toml
|
||||||
|
schema_version = 1
|
||||||
|
|
||||||
|
name = "My DeepSeek Chat"
|
||||||
|
description = "DeepSeek V3 via an OpenAI-compatible endpoint."
|
||||||
|
|
||||||
|
provider_instance = "OpenAI Compatible"
|
||||||
|
model = "deepseek-chat"
|
||||||
|
endpoint = "/chat/completions"
|
||||||
|
|
||||||
|
system_prompt = """{{ agent_role() }}"""
|
||||||
|
|
||||||
|
[body]
|
||||||
|
max_tokens = 8192
|
||||||
|
temperature = 0.7
|
||||||
|
stream = true
|
||||||
|
messages = """
|
||||||
|
[ {% include "partials/openai_messages.jinja" %} ]
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
Shorter still — extend a bundled provider base and state only the delta:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
schema_version = 1
|
||||||
|
|
||||||
|
extends = "OpenAI Compatible Base Chat"
|
||||||
|
name = "My DeepSeek Chat"
|
||||||
|
model = "deepseek-chat"
|
||||||
|
```
|
||||||
|
|
||||||
|
Bundled agents themselves are read-only. To get a variant of a preset, create
|
||||||
|
your own agent under a new name and put it where you want it in the roster:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
schema_version = 1
|
||||||
|
|
||||||
|
extends = "Mistral Base Chat"
|
||||||
|
name = "My Mistral (low temp)"
|
||||||
|
model = "mistral-small-latest"
|
||||||
|
|
||||||
|
[body]
|
||||||
|
temperature = 0.3
|
||||||
|
```
|
||||||
|
|
||||||
|
If all you want is a different model for a preset, you don't need a file at
|
||||||
|
all — set the per-agent model override in the settings UI.
|
||||||
|
|
||||||
|
## Key reference
|
||||||
|
|
||||||
|
| Key | Required | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| `schema_version` | no (default 1) | Format version; the plugin refuses files newer than it supports. |
|
||||||
|
| `name` | yes | Unique identifier; shown in the UI, referenced by rosters and `extends`. |
|
||||||
|
| `description` | no | Tooltip text in the Agents list. |
|
||||||
|
| `provider_instance` | yes* | Name of a provider instance (see below). |
|
||||||
|
| `model` | yes* | Default model; can be overridden per agent in settings. |
|
||||||
|
| `endpoint` | yes* | Path appended to the provider instance URL. May contain `${MODEL}` (e.g. Google: `/models/${MODEL}:streamGenerateContent?alt=sse`). |
|
||||||
|
| `system_prompt` | no | Jinja template for the system prompt (see building blocks below). FIM agents usually omit it. |
|
||||||
|
| `tags` | no | Free-form strings shown as pills in the UI for discoverability. |
|
||||||
|
| `enable_thinking` | no | Capability hint (UI badge). The `[body]` is the source of truth for what is sent. |
|
||||||
|
| `enable_tools` | no | Lets the provider inject tool definitions into the request. |
|
||||||
|
| `cache_prompt` / `cache_ttl` | no | Prompt caching (Anthropic); `cache_ttl = "1h"` selects the long TTL. |
|
||||||
|
| `extends` | no | Name of a parent agent to inherit from. |
|
||||||
|
| `abstract` | no | Mark as template-only: it can be extended but is never loaded as a usable agent. Not inherited. |
|
||||||
|
| `hidden` | no | Loaded and usable, but not listed in selection UIs. Not inherited. |
|
||||||
|
| `[match]` | no | Routing constraints (see Routing). |
|
||||||
|
| `[body]` | yes* | The literal request body (see below). |
|
||||||
|
|
||||||
|
\* required after `extends` resolution — a child inherits these from its
|
||||||
|
parent, so it only states what differs.
|
||||||
|
|
||||||
|
### Required keys checked at load
|
||||||
|
|
||||||
|
A concrete (non-abstract) agent must end up with `name`,
|
||||||
|
`provider_instance`, `model`, `endpoint`, and a non-empty `[body]`. Unknown
|
||||||
|
keys anywhere at the top level or in `[match]` produce a warning — this
|
||||||
|
catches typos like `enable_thinkin`.
|
||||||
|
|
||||||
|
## Provider instances
|
||||||
|
|
||||||
|
`provider_instance` refers to a provider configuration (URL + API key
|
||||||
|
reference + client API). Bundled instances:
|
||||||
|
|
||||||
|
`Claude`, `Codestral`, `Google AI`, `llama.cpp`,
|
||||||
|
`LM Studio (Chat Completions)`, `LM Studio (Responses API)`, `Mistral AI`,
|
||||||
|
`Ollama (Native)`, `Ollama (OpenAI-compatible)`, `OpenAI (Chat Completions)`,
|
||||||
|
`OpenAI (Responses API)`, `OpenAI Compatible`, `OpenRouter`.
|
||||||
|
|
||||||
|
User-defined instances live next to agents in
|
||||||
|
`…/qodeassist/config/providers/*.toml` and follow the same
|
||||||
|
override-by-name layering.
|
||||||
|
|
||||||
|
## `extends` — inheriting from another agent
|
||||||
|
|
||||||
|
A child deep-merges over its parent: scalar keys are replaced, tables (such
|
||||||
|
as `[body]` and `[body.options]`) merge key-by-key, and **arrays are replaced
|
||||||
|
whole** (a child that wants the parent's `tags` plus one more must restate
|
||||||
|
the full list). Chains can be deeper than one level; cycles and missing
|
||||||
|
parents are load errors.
|
||||||
|
|
||||||
|
`abstract` and `hidden` are never inherited — extending a hidden agent
|
||||||
|
yields a visible child unless the child says otherwise.
|
||||||
|
|
||||||
|
Every provider ships an abstract base that already carries the provider
|
||||||
|
instance, endpoint, and request body — extending one and setting `model` is
|
||||||
|
usually all a custom agent needs:
|
||||||
|
|
||||||
|
| Base | Provider / API |
|
||||||
|
|---|---|
|
||||||
|
| `Anthropic Base Chat` | Claude, Anthropic Messages (`/v1/messages`) |
|
||||||
|
| `OpenAI Base Chat` | OpenAI, Chat Completions (`/chat/completions`) |
|
||||||
|
| `OpenAI Responses Base` | OpenAI, Responses API (`/responses`) |
|
||||||
|
| `OpenAI Compatible Base Chat` | Any OpenAI-compatible server |
|
||||||
|
| `Google Base Chat` | Google AI, Gemini `generateContent` |
|
||||||
|
| `Mistral Base Chat` | Mistral AI, Chat Completions |
|
||||||
|
| `Codestral Base Chat` | Codestral, Chat Completions |
|
||||||
|
| `Codestral FIM Base` | Mistral AI, `/v1/fim/completions` code completion |
|
||||||
|
| `OpenRouter Base Chat` | OpenRouter, Chat Completions |
|
||||||
|
| `LM Studio Base Chat` | LM Studio, Chat Completions |
|
||||||
|
| `LM Studio Responses Base` | LM Studio, Responses API |
|
||||||
|
| `llama.cpp Base Chat` | llama.cpp server, Chat Completions |
|
||||||
|
| `Ollama Base Chat` | Ollama, native `/api/chat` |
|
||||||
|
| `Ollama (OpenAI-compatible) Base Chat` | Ollama, OpenAI-compatible endpoint |
|
||||||
|
| `Ollama FIM Base` | Ollama, native `/api/generate` fill-in-the-middle |
|
||||||
|
|
||||||
|
Concrete agents work as parents too — `extends = "Claude Sonnet 4.6 Chat"`
|
||||||
|
inherits everything including the model.
|
||||||
|
|
||||||
|
## `[body]` — the request, literally
|
||||||
|
|
||||||
|
`[body]` is the request body, written exactly like the provider's curl
|
||||||
|
example. Per key, recursively:
|
||||||
|
|
||||||
|
- **string containing jinja** (`{{` or `{%`) — rendered, and the output is
|
||||||
|
spliced in as raw JSON. A render that produces nothing drops the key.
|
||||||
|
- **plain string / number / bool / table** — passed through unchanged.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[body]
|
||||||
|
max_tokens = 16000
|
||||||
|
stream = true
|
||||||
|
thinking = { type = "adaptive", display = "summarized" }
|
||||||
|
messages = """
|
||||||
|
[ {% include "partials/anthropic_messages.jinja" %} ]
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
There are no runtime toggles: a thinking variant is a separate agent file
|
||||||
|
that carries the thinking fields in its body.
|
||||||
|
|
||||||
|
Every agent body is dry-run rendered at load against a synthetic
|
||||||
|
conversation (text, thinking, tool calls, tool results, images), so jinja
|
||||||
|
syntax errors, unknown callbacks, missing partials, and invalid JSON are
|
||||||
|
reported at startup — not mid-conversation. Trailing commas emitted by loops
|
||||||
|
are stripped automatically; don't bother with `loop.is_last` bookkeeping.
|
||||||
|
|
||||||
|
### Template data (`ctx`)
|
||||||
|
|
||||||
|
| Field | Content |
|
||||||
|
|---|---|
|
||||||
|
| `ctx.system_prompt` | Rendered system prompt (present only if the agent has one). |
|
||||||
|
| `ctx.prefix` / `ctx.suffix` | Code around the cursor (FIM/completion sessions). |
|
||||||
|
| `ctx.files_metadata` | Array of `{ file_path, content }` for attached files. |
|
||||||
|
| `ctx.history` | Array of messages: `{ role, content, content_blocks, images? }`. |
|
||||||
|
|
||||||
|
`content` is the message's flattened text; `content_blocks` is the
|
||||||
|
structured form:
|
||||||
|
|
||||||
|
| `type` | Fields |
|
||||||
|
|---|---|
|
||||||
|
| `text` | `text` |
|
||||||
|
| `thinking` | `thinking`, `signature` |
|
||||||
|
| `redacted_thinking` | `data` |
|
||||||
|
| `tool_use` | `id`, `name`, `input` (JSON object) |
|
||||||
|
| `tool_result` | `tool_use_id`, `content`, `name` |
|
||||||
|
| `image` | `data`, `media_type`, `is_url` |
|
||||||
|
|
||||||
|
### Callbacks available in `[body]`
|
||||||
|
|
||||||
|
| Callback | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `tojson(x)` | Serialize any value as JSON (correct quoting/escaping). Use it for every interpolated value. |
|
||||||
|
| `filter_by_type(blocks, "tool_use")` | Subset of `content_blocks` with the given type. |
|
||||||
|
| `filter_skip_role(history, "system")` | History without messages of a role. |
|
||||||
|
| `strip_signature_suffix(s)` | Remove a trailing `[Signature: …]` marker. |
|
||||||
|
|
||||||
|
### Partials and `{% include %}`
|
||||||
|
|
||||||
|
The repetitive message-array rendering lives in shared partials. Includes
|
||||||
|
resolve against the bundled set first, then the user agent's own directory —
|
||||||
|
so a user agent can both reuse bundled partials and ship its own next to its
|
||||||
|
TOML (e.g. `partials/my_messages.jinja`). Paths with `..` or a leading `/`
|
||||||
|
are rejected.
|
||||||
|
|
||||||
|
Bundled partials: `partials/openai_messages.jinja`,
|
||||||
|
`partials/openai_responses_input.jinja`, `partials/anthropic_messages.jinja`,
|
||||||
|
`partials/google_contents.jinja`, `partials/ollama_messages.jinja` (plus the
|
||||||
|
per-message helpers they include).
|
||||||
|
|
||||||
|
## `system_prompt` — composable building blocks
|
||||||
|
|
||||||
|
`system_prompt` is itself a jinja template, rendered with:
|
||||||
|
|
||||||
|
| Helper | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `{{ agent_role() }}` | Text of the role currently selected in the chat (falls back to `developer`). |
|
||||||
|
| `{{ agent_role("reviewer") }}` | A specific role by id (Settings → QodeAssist → Roles). |
|
||||||
|
| `{{ read_file("${PROJECT_DIR}/STYLE.md") }}` | Inline a file. Reads are restricted to the project directory and `~/qodeassist`. |
|
||||||
|
| `{{ file_exists(p) }}` / `{{ read_dir(p) }}` | Existence check / directory listing (same root restrictions). |
|
||||||
|
| `{{ head_lines(s, n) }}` | First `n` lines of a string. |
|
||||||
|
| `basename`, `dirname`, `ext`, `lower`, `upper` | Path/string helpers. |
|
||||||
|
| `${PROJECT_DIR}`, `${HOME}` | Substituted before rendering. |
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
system_prompt = """
|
||||||
|
{{ agent_role() }}
|
||||||
|
|
||||||
|
{% if file_exists("${PROJECT_DIR}/.qodeassist-style.md") %}
|
||||||
|
Project conventions:
|
||||||
|
{{ read_file("${PROJECT_DIR}/.qodeassist-style.md") }}
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
## Routing — `[match]` and rosters
|
||||||
|
|
||||||
|
Each pipeline (code completion, chat, compression, quick refactor) has an
|
||||||
|
ordered roster of agents. For the current file, the **first roster entry
|
||||||
|
whose `[match]` accepts** wins.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[match]
|
||||||
|
file_patterns = ["*.qml", "*.js"]
|
||||||
|
path_patterns = ["*/tests/*"]
|
||||||
|
project_names = ["MyProject"]
|
||||||
|
```
|
||||||
|
|
||||||
|
- Dimensions are ANDed; an empty dimension is unconstrained; an entirely
|
||||||
|
empty/absent `[match]` is a catch-all.
|
||||||
|
- `file_patterns` are case-insensitive globs tested against the file name
|
||||||
|
and the full path; `path_patterns` against the full path only.
|
||||||
|
- `project_names` are exact, case-sensitive project names.
|
||||||
|
|
||||||
|
Typical setup: a specialized agent (e.g. `Qt CodeLlama 13B QML FIM` with
|
||||||
|
`*.qml`) first, a catch-all agent last.
|
||||||
|
|
||||||
|
## Models
|
||||||
|
|
||||||
|
The TOML `model` is only the default. The settings UI can set a per-agent
|
||||||
|
override (stored in `agent_models.json`); the resolved model is also
|
||||||
|
substituted into `${MODEL}` in `endpoint` before sending.
|
||||||
|
|
||||||
|
## Contributing your agent to QodeAssist
|
||||||
|
|
||||||
|
The bundled agent set grows through contributions — if you've made an agent
|
||||||
|
for a provider or model that others could use, please send it upstream
|
||||||
|
instead of keeping it local. No C++ is needed:
|
||||||
|
|
||||||
|
1. Develop and verify the agent locally in the user agents directory.
|
||||||
|
2. In a fork, copy the TOML to `sources/agents/` and register the file in
|
||||||
|
`sources/agents/agents.qrc`.
|
||||||
|
3. Keep it a thin delta: extend the matching provider base and set only
|
||||||
|
`name`, `description`, `model`, `tags` (and `[body]` keys that genuinely
|
||||||
|
differ). Look at `mistral_chat.toml` or `ollama_qwen25_coder_fim.toml`
|
||||||
|
for the expected shape.
|
||||||
|
4. Run the tests (`QodeAssistTest`): `BundledAgentsTest` automatically
|
||||||
|
loads every bundled agent, resolves its `extends` chain, and dry-renders
|
||||||
|
its `[body]` — if your TOML passes, it works.
|
||||||
|
5. Open a pull request.
|
||||||
|
|
||||||
|
Conventions:
|
||||||
|
|
||||||
|
- File name: `<provider>_<model_or_purpose>_<kind>.toml`
|
||||||
|
(e.g. `ollama_qwen25_coder_fim.toml`).
|
||||||
|
- `name` is user-visible and must be unique; include the provider and model
|
||||||
|
(e.g. `Ollama Qwen2.5-Coder FIM`).
|
||||||
|
- Specialized completion agents should carry a `[match]` block so routing
|
||||||
|
can pick them automatically (e.g. `file_patterns = ["*.qml"]`).
|
||||||
|
- A whole new provider with an OpenAI-compatible API is also TOML-only: a
|
||||||
|
provider instance file in `sources/providersConfig/`, one abstract
|
||||||
|
`<Provider> Base Chat`, and concrete agents on top. New request/response
|
||||||
|
*formats* are the only thing that needs C++.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Agent missing from the list** — check General Messages for `[Agents]
|
||||||
|
error:` lines; the file failed to parse, resolve, or validate.
|
||||||
|
- **`… has the same name as a bundled agent — bundled agents cannot be
|
||||||
|
replaced`** — pick a different `name`; use `extends` to inherit from the
|
||||||
|
bundled agent instead.
|
||||||
|
- **`Unknown key 'x' … ignored (typo?)`** — the key isn't part of the
|
||||||
|
schema; compare with the table above.
|
||||||
|
- **`Agent 'X' extends unknown agent 'Y'`** — the parent's `name` (not file
|
||||||
|
name) must match exactly; the parent must be bundled or in the same
|
||||||
|
directory.
|
||||||
|
- **`[body] failed to render to valid JSON`** — the dry run failed; the log
|
||||||
|
contains the rendered snippet. Usually a missing `tojson(...)` around an
|
||||||
|
interpolated string.
|
||||||
|
- **Edits not picked up** — agents are loaded at startup; restart
|
||||||
|
Qt Creator.
|
||||||
@@ -48,7 +48,6 @@ struct AgentConfig
|
|||||||
bool hidden = false;
|
bool hidden = false;
|
||||||
|
|
||||||
QString sourcePath;
|
QString sourcePath;
|
||||||
bool overridesBundled = false;
|
|
||||||
bool isUserSource() const { return !sourcePath.startsWith(QLatin1StringView{":/"}); }
|
bool isUserSource() const { return !sourcePath.startsWith(QLatin1StringView{":/"}); }
|
||||||
|
|
||||||
static QString validate(const AgentConfig &config);
|
static QString validate(const AgentConfig &config);
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ void AgentFactory::reload()
|
|||||||
Q_ASSERT(thread() == QThread::currentThread());
|
Q_ASSERT(thread() == QThread::currentThread());
|
||||||
clear();
|
clear();
|
||||||
|
|
||||||
|
QDir().mkpath(userAgentsDir());
|
||||||
auto result = Agents::AgentLoader::load(agentQrcPrefix(), userAgentsDir());
|
auto result = Agents::AgentLoader::load(agentQrcPrefix(), userAgentsDir());
|
||||||
for (const QString &err : result.errors)
|
for (const QString &err : result.errors)
|
||||||
LOG_MESSAGE(QString("[Agents] error: %1").arg(err));
|
LOG_MESSAGE(QString("[Agents] error: %1").arg(err));
|
||||||
|
|||||||
@@ -145,16 +145,49 @@ struct RawEntry
|
|||||||
{
|
{
|
||||||
QJsonObject obj;
|
QJsonObject obj;
|
||||||
QString filePath;
|
QString filePath;
|
||||||
bool overridesBundled = false;
|
bool isUserLayer = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr int kMaxExtendsDepth = 32;
|
constexpr int kMaxExtendsDepth = 32;
|
||||||
|
|
||||||
|
void lintUnknownKeys(const QJsonObject &obj, const QString &filePath, QStringList &warnings)
|
||||||
|
{
|
||||||
|
static const QSet<QString> kTopLevelKeys = {
|
||||||
|
QStringLiteral("schema_version"), QStringLiteral("name"),
|
||||||
|
QStringLiteral("description"), QStringLiteral("provider_instance"),
|
||||||
|
QStringLiteral("model"), QStringLiteral("endpoint"),
|
||||||
|
QStringLiteral("system_prompt"), QStringLiteral("tags"),
|
||||||
|
QStringLiteral("match"), QStringLiteral("enable_thinking"),
|
||||||
|
QStringLiteral("enable_tools"), QStringLiteral("cache_prompt"),
|
||||||
|
QStringLiteral("cache_ttl"), QStringLiteral("body"),
|
||||||
|
QStringLiteral("extends"), QStringLiteral("abstract"),
|
||||||
|
QStringLiteral("hidden")};
|
||||||
|
static const QSet<QString> kMatchKeys = {
|
||||||
|
QStringLiteral("file_patterns"),
|
||||||
|
QStringLiteral("path_patterns"),
|
||||||
|
QStringLiteral("project_names")};
|
||||||
|
|
||||||
|
for (auto it = obj.constBegin(); it != obj.constEnd(); ++it) {
|
||||||
|
if (!kTopLevelKeys.contains(it.key())) {
|
||||||
|
warnings.append(QStringLiteral("Unknown key '%1' in %2 — ignored (typo?)")
|
||||||
|
.arg(it.key(), filePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const QJsonObject matchObj = obj.value("match").toObject();
|
||||||
|
for (auto it = matchObj.constBegin(); it != matchObj.constEnd(); ++it) {
|
||||||
|
if (!kMatchKeys.contains(it.key())) {
|
||||||
|
warnings.append(QStringLiteral("Unknown key 'match.%1' in %2 — ignored (typo?)")
|
||||||
|
.arg(it.key(), filePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void scanDir(
|
void scanDir(
|
||||||
const QString &dir,
|
const QString &dir,
|
||||||
bool isUserLayer,
|
bool isUserLayer,
|
||||||
QHash<QString, RawEntry> &raw,
|
QHash<QString, RawEntry> &raw,
|
||||||
QStringList &errors)
|
QStringList &errors,
|
||||||
|
QStringList *warnings)
|
||||||
{
|
{
|
||||||
if (dir.isEmpty()) return;
|
if (dir.isEmpty()) return;
|
||||||
QDir d(dir);
|
QDir d(dir);
|
||||||
@@ -173,11 +206,39 @@ void scanDir(
|
|||||||
errors.append(QStringLiteral("Agent at %1 has no 'name'").arg(fullPath));
|
errors.append(QStringLiteral("Agent at %1 has no 'name'").arg(fullPath));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const bool overrides = isUserLayer && raw.contains(name);
|
if (warnings)
|
||||||
raw.insert(name, {*objOpt, fullPath, overrides});
|
lintUnknownKeys(*objOpt, fullPath, *warnings);
|
||||||
|
const auto existing = raw.constFind(name);
|
||||||
|
if (existing != raw.constEnd() && existing->isUserLayer != isUserLayer) {
|
||||||
|
errors.append(
|
||||||
|
QStringLiteral("Agent '%1' at %2 has the same name as a bundled agent — "
|
||||||
|
"bundled agents cannot be replaced; rename it and use "
|
||||||
|
"'extends' to build on the bundled one")
|
||||||
|
.arg(name, fullPath));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (warnings && existing != raw.constEnd()) {
|
||||||
|
warnings->append(
|
||||||
|
QStringLiteral("Agent '%1' is defined in both %2 and %3 — %3 wins")
|
||||||
|
.arg(name, existing->filePath, fullPath));
|
||||||
|
}
|
||||||
|
raw.insert(name, {*objOpt, fullPath, isUserLayer});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QJsonObject mergeChild(const QJsonObject &parentMerged, const QJsonObject &self, const QString &name)
|
||||||
|
{
|
||||||
|
QJsonObject merged = deepMerge(parentMerged, self);
|
||||||
|
merged["name"] = name;
|
||||||
|
for (const QString &key : {QStringLiteral("abstract"), QStringLiteral("hidden")}) {
|
||||||
|
if (self.contains(key))
|
||||||
|
merged[key] = self.value(key);
|
||||||
|
else
|
||||||
|
merged.remove(key);
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
QJsonObject resolveExtends(
|
QJsonObject resolveExtends(
|
||||||
const QString &name,
|
const QString &name,
|
||||||
const QHash<QString, RawEntry> &raw,
|
const QHash<QString, RawEntry> &raw,
|
||||||
@@ -196,7 +257,7 @@ QJsonObject resolveExtends(
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
if (!raw.contains(name)) {
|
if (!raw.contains(name)) {
|
||||||
errors.append(QStringLiteral("Unknown parent agent '%1'").arg(name));
|
errors.append(QStringLiteral("Unknown agent '%1'").arg(name));
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
visiting.insert(name);
|
visiting.insert(name);
|
||||||
@@ -204,15 +265,15 @@ QJsonObject resolveExtends(
|
|||||||
QJsonObject self = raw.value(name).obj;
|
QJsonObject self = raw.value(name).obj;
|
||||||
const QString parent = self.value("extends").toString();
|
const QString parent = self.value("extends").toString();
|
||||||
if (!parent.isEmpty()) {
|
if (!parent.isEmpty()) {
|
||||||
|
if (!raw.contains(parent)) {
|
||||||
|
errors.append(QStringLiteral("Agent '%1' extends unknown agent '%2' (%3)")
|
||||||
|
.arg(name, parent, raw.value(name).filePath));
|
||||||
|
visiting.remove(name);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
const QJsonObject parentMerged
|
const QJsonObject parentMerged
|
||||||
= resolveExtends(parent, raw, visiting, errors, depth + 1);
|
= resolveExtends(parent, raw, visiting, errors, depth + 1);
|
||||||
QJsonObject merged = deepMerge(parentMerged, self);
|
self = mergeChild(parentMerged, self, name);
|
||||||
merged["name"] = name;
|
|
||||||
if (self.contains("abstract"))
|
|
||||||
merged["abstract"] = self.value("abstract");
|
|
||||||
else
|
|
||||||
merged.remove("abstract");
|
|
||||||
self = merged;
|
|
||||||
}
|
}
|
||||||
visiting.remove(name);
|
visiting.remove(name);
|
||||||
return self;
|
return self;
|
||||||
@@ -224,7 +285,7 @@ std::optional<AgentConfig> AgentLoader::parseFile(
|
|||||||
const QString &path,
|
const QString &path,
|
||||||
const QString &qrcPrefix,
|
const QString &qrcPrefix,
|
||||||
QString *error,
|
QString *error,
|
||||||
QStringList * /*warnings*/)
|
QStringList *warnings)
|
||||||
{
|
{
|
||||||
auto objOpt = parseTomlFile(path, error);
|
auto objOpt = parseTomlFile(path, error);
|
||||||
if (!objOpt) return std::nullopt;
|
if (!objOpt) return std::nullopt;
|
||||||
@@ -234,12 +295,14 @@ std::optional<AgentConfig> AgentLoader::parseFile(
|
|||||||
if (error) *error = QStringLiteral("Agent at %1 has no 'name'").arg(path);
|
if (error) *error = QStringLiteral("Agent at %1 has no 'name'").arg(path);
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
if (warnings)
|
||||||
|
lintUnknownKeys(*objOpt, path, *warnings);
|
||||||
|
|
||||||
QHash<QString, RawEntry> raw;
|
QHash<QString, RawEntry> raw;
|
||||||
QStringList scanErrors;
|
QStringList scanErrors;
|
||||||
scanDir(qrcPrefix, /*isUserLayer=*/false, raw, scanErrors);
|
scanDir(qrcPrefix, /*isUserLayer=*/false, raw, scanErrors, nullptr);
|
||||||
scanDir(QFileInfo(path).absolutePath(), /*isUserLayer=*/true, raw, scanErrors);
|
scanDir(QFileInfo(path).absolutePath(), /*isUserLayer=*/true, raw, scanErrors, nullptr);
|
||||||
raw.insert(name, {*objOpt, path, raw.contains(name)});
|
raw.insert(name, {*objOpt, path, true});
|
||||||
|
|
||||||
QSet<QString> visiting;
|
QSet<QString> visiting;
|
||||||
QStringList resolveErrors;
|
QStringList resolveErrors;
|
||||||
@@ -270,8 +333,8 @@ AgentLoader::LoadResult AgentLoader::load(const QString &qrcPrefix, const QStrin
|
|||||||
LoadResult result;
|
LoadResult result;
|
||||||
QHash<QString, RawEntry> raw;
|
QHash<QString, RawEntry> raw;
|
||||||
|
|
||||||
scanDir(qrcPrefix, /*isUserLayer=*/false, raw, result.errors);
|
scanDir(qrcPrefix, /*isUserLayer=*/false, raw, result.errors, &result.warnings);
|
||||||
scanDir(userDir, /*isUserLayer=*/true, raw, result.errors);
|
scanDir(userDir, /*isUserLayer=*/true, raw, result.errors, &result.warnings);
|
||||||
|
|
||||||
for (auto it = raw.constBegin(); it != raw.constEnd(); ++it) {
|
for (auto it = raw.constBegin(); it != raw.constEnd(); ++it) {
|
||||||
const QString &name = it.key();
|
const QString &name = it.key();
|
||||||
@@ -282,7 +345,6 @@ AgentLoader::LoadResult AgentLoader::load(const QString &qrcPrefix, const QStrin
|
|||||||
|
|
||||||
AgentConfig cfg = configFromMerged(merged);
|
AgentConfig cfg = configFromMerged(merged);
|
||||||
cfg.sourcePath = it.value().filePath;
|
cfg.sourcePath = it.value().filePath;
|
||||||
cfg.overridesBundled = it.value().overridesBundled;
|
|
||||||
|
|
||||||
if (cfg.abstract) continue;
|
if (cfg.abstract) continue;
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
#include "AgentRouter.hpp"
|
#include "AgentRouter.hpp"
|
||||||
|
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QMutex>
|
||||||
|
#include <QMutexLocker>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
|
|
||||||
#include "AgentFactory.hpp"
|
#include "AgentFactory.hpp"
|
||||||
@@ -13,16 +16,29 @@ namespace QodeAssist::AgentRouter {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
QRegularExpression compiledGlob(const QString &pattern)
|
||||||
|
{
|
||||||
|
static QHash<QString, QRegularExpression> cache;
|
||||||
|
static QMutex mutex;
|
||||||
|
QMutexLocker lock(&mutex);
|
||||||
|
const auto it = cache.constFind(pattern);
|
||||||
|
if (it != cache.constEnd())
|
||||||
|
return *it;
|
||||||
|
const QRegularExpression re(
|
||||||
|
QRegularExpression::anchoredPattern(
|
||||||
|
QRegularExpression::wildcardToRegularExpression(
|
||||||
|
pattern, QRegularExpression::NonPathWildcardConversion)),
|
||||||
|
QRegularExpression::CaseInsensitiveOption);
|
||||||
|
cache.insert(pattern, re);
|
||||||
|
return re;
|
||||||
|
}
|
||||||
|
|
||||||
bool matchesAnyGlob(const QStringList &patterns, const QString &subject)
|
bool matchesAnyGlob(const QStringList &patterns, const QString &subject)
|
||||||
{
|
{
|
||||||
if (subject.isEmpty())
|
if (subject.isEmpty())
|
||||||
return false;
|
return false;
|
||||||
for (const QString &pat : patterns) {
|
for (const QString &pat : patterns) {
|
||||||
const QRegularExpression re(
|
const QRegularExpression re = compiledGlob(pat);
|
||||||
QRegularExpression::anchoredPattern(
|
|
||||||
QRegularExpression::wildcardToRegularExpression(
|
|
||||||
pat, QRegularExpression::NonPathWildcardConversion)),
|
|
||||||
QRegularExpression::CaseInsensitiveOption);
|
|
||||||
if (re.isValid() && re.match(subject).hasMatch())
|
if (re.isValid() && re.match(subject).hasMatch())
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,15 @@
|
|||||||
<file>google_base_chat.toml</file>
|
<file>google_base_chat.toml</file>
|
||||||
<file>ollama_base_chat.toml</file>
|
<file>ollama_base_chat.toml</file>
|
||||||
<file>ollama_base_fim.toml</file>
|
<file>ollama_base_fim.toml</file>
|
||||||
|
<file>mistral_base_chat.toml</file>
|
||||||
|
<file>codestral_base_chat.toml</file>
|
||||||
|
<file>codestral_fim_base.toml</file>
|
||||||
|
<file>openrouter_base_chat.toml</file>
|
||||||
|
<file>lmstudio_base_chat.toml</file>
|
||||||
|
<file>lmstudio_responses_base.toml</file>
|
||||||
|
<file>llamacpp_base_chat.toml</file>
|
||||||
|
<file>ollama_openai_base_chat.toml</file>
|
||||||
|
<file>openai_compatible_base_chat.toml</file>
|
||||||
|
|
||||||
<file>openai_chat.toml</file>
|
<file>openai_chat.toml</file>
|
||||||
<file>openai_compatible_chat.toml</file>
|
<file>openai_compatible_chat.toml</file>
|
||||||
@@ -35,8 +44,8 @@
|
|||||||
<file>ollama_gemma4_e4b_chat.toml</file>
|
<file>ollama_gemma4_e4b_chat.toml</file>
|
||||||
<file>ollama_codellama_7b_code_fim.toml</file>
|
<file>ollama_codellama_7b_code_fim.toml</file>
|
||||||
<file>ollama_codellama_13b_qml_fim.toml</file>
|
<file>ollama_codellama_13b_qml_fim.toml</file>
|
||||||
|
<file>ollama_qwen25_coder_fim.toml</file>
|
||||||
|
|
||||||
<file>claude_sonnet_chat.toml</file>
|
|
||||||
<file>claude_sonnet46_chat.toml</file>
|
<file>claude_sonnet46_chat.toml</file>
|
||||||
<file>claude_haiku45_chat.toml</file>
|
<file>claude_haiku45_chat.toml</file>
|
||||||
<file>claude_opus_max.toml</file>
|
<file>claude_opus_max.toml</file>
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
schema_version = 1
|
|
||||||
|
|
||||||
extends = "Anthropic Base Chat"
|
|
||||||
name = "Claude Sonnet Chat"
|
|
||||||
description = "Anthropic Claude Sonnet 4.6 (Messages API) — coding chat assistant with thinking."
|
|
||||||
|
|
||||||
model = "claude-sonnet-4-6"
|
|
||||||
|
|
||||||
enable_thinking = true
|
|
||||||
tags = ["chat", "claude", "anthropic", "cloud"]
|
|
||||||
|
|
||||||
[body]
|
|
||||||
max_tokens = 8192
|
|
||||||
thinking = { type = "enabled", budget_tokens = 4096 }
|
|
||||||
11
sources/agents/codestral_base_chat.toml
Normal file
11
sources/agents/codestral_base_chat.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
schema_version = 1
|
||||||
|
|
||||||
|
extends = "OpenAI Base Chat"
|
||||||
|
name = "Codestral Base Chat"
|
||||||
|
description = "Mistral Codestral Chat Completions. Abstract — extend it and set model."
|
||||||
|
abstract = true
|
||||||
|
|
||||||
|
provider_instance = "Codestral"
|
||||||
|
endpoint = "/v1/chat/completions"
|
||||||
|
|
||||||
|
tags = ["chat", "codestral", "mistral", "cloud"]
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
schema_version = 1
|
schema_version = 1
|
||||||
|
|
||||||
extends = "OpenAI Base Chat"
|
extends = "Codestral Base Chat"
|
||||||
name = "Codestral Chat"
|
name = "Codestral Chat"
|
||||||
description = "Mistral Codestral (Chat Completions API) — coding chat assistant."
|
description = "Mistral Codestral (Chat Completions API) — coding chat assistant."
|
||||||
|
|
||||||
provider_instance = "Codestral"
|
model = "codestral-latest"
|
||||||
endpoint = "/v1/chat/completions"
|
|
||||||
model = "codestral-latest"
|
|
||||||
|
|
||||||
tags = ["chat", "codestral", "mistral", "cloud"]
|
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
schema_version = 1
|
schema_version = 1
|
||||||
|
|
||||||
|
extends = "Codestral FIM Base"
|
||||||
name = "Codestral FIM"
|
name = "Codestral FIM"
|
||||||
description = "Mistral Codestral fill-in-the-middle code completion (/v1/fim/completions)."
|
description = "Mistral Codestral fill-in-the-middle code completion (/v1/fim/completions)."
|
||||||
|
|
||||||
provider_instance = "Mistral AI"
|
model = "codestral-latest"
|
||||||
endpoint = "/v1/fim/completions"
|
|
||||||
model = "codestral-latest"
|
|
||||||
|
|
||||||
enable_thinking = false
|
|
||||||
enable_tools = false
|
|
||||||
tags = ["fim", "codestral", "mistral", "cloud", "completion"]
|
tags = ["fim", "codestral", "mistral", "cloud", "completion"]
|
||||||
|
|
||||||
[body]
|
|
||||||
max_tokens = 256
|
|
||||||
temperature = 0.2
|
|
||||||
stream = true
|
|
||||||
prompt = """{{ tojson(ctx.prefix) }}"""
|
|
||||||
suffix = """{% if existsIn(ctx, "suffix") %}{{ tojson(ctx.suffix) }}{% endif %}"""
|
|
||||||
|
|||||||
17
sources/agents/codestral_fim_base.toml
Normal file
17
sources/agents/codestral_fim_base.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
schema_version = 1
|
||||||
|
|
||||||
|
name = "Codestral FIM Base"
|
||||||
|
description = "Mistral fill-in-the-middle completion (/v1/fim/completions). Abstract — extend it and set model."
|
||||||
|
abstract = true
|
||||||
|
|
||||||
|
provider_instance = "Mistral AI"
|
||||||
|
endpoint = "/v1/fim/completions"
|
||||||
|
|
||||||
|
tags = ["fim", "mistral", "cloud", "completion"]
|
||||||
|
|
||||||
|
[body]
|
||||||
|
max_tokens = 256
|
||||||
|
temperature = 0.2
|
||||||
|
stream = true
|
||||||
|
prompt = """{{ tojson(ctx.prefix) }}"""
|
||||||
|
suffix = """{% if existsIn(ctx, "suffix") %}{{ tojson(ctx.suffix) }}{% endif %}"""
|
||||||
11
sources/agents/llamacpp_base_chat.toml
Normal file
11
sources/agents/llamacpp_base_chat.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
schema_version = 1
|
||||||
|
|
||||||
|
extends = "OpenAI Base Chat"
|
||||||
|
name = "llama.cpp Base Chat"
|
||||||
|
description = "llama.cpp server (OpenAI-compatible Chat Completions). Abstract — extend it and set model."
|
||||||
|
abstract = true
|
||||||
|
|
||||||
|
provider_instance = "llama.cpp"
|
||||||
|
endpoint = "/v1/chat/completions"
|
||||||
|
|
||||||
|
tags = ["chat", "llamacpp", "local"]
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
schema_version = 1
|
schema_version = 1
|
||||||
|
|
||||||
extends = "OpenAI Base Chat"
|
extends = "llama.cpp Base Chat"
|
||||||
name = "llama.cpp Chat"
|
name = "llama.cpp Chat"
|
||||||
description = "llama.cpp server (OpenAI-compatible Chat Completions) — local coding chat assistant."
|
description = "llama.cpp server (OpenAI-compatible Chat Completions) — local coding chat assistant."
|
||||||
|
|
||||||
provider_instance = "llama.cpp"
|
model = "llama"
|
||||||
endpoint = "/v1/chat/completions"
|
|
||||||
model = "llama"
|
|
||||||
|
|
||||||
tags = ["chat", "llamacpp", "local"]
|
|
||||||
|
|||||||
11
sources/agents/lmstudio_base_chat.toml
Normal file
11
sources/agents/lmstudio_base_chat.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
schema_version = 1
|
||||||
|
|
||||||
|
extends = "OpenAI Base Chat"
|
||||||
|
name = "LM Studio Base Chat"
|
||||||
|
description = "LM Studio Chat Completions. Abstract — extend it and set model."
|
||||||
|
abstract = true
|
||||||
|
|
||||||
|
provider_instance = "LM Studio (Chat Completions)"
|
||||||
|
endpoint = "/v1/chat/completions"
|
||||||
|
|
||||||
|
tags = ["chat", "lmstudio", "local"]
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
schema_version = 1
|
schema_version = 1
|
||||||
|
|
||||||
extends = "OpenAI Base Chat"
|
extends = "LM Studio Base Chat"
|
||||||
name = "LM Studio Chat"
|
name = "LM Studio Chat"
|
||||||
description = "LM Studio (Chat Completions API) — local coding chat assistant."
|
description = "LM Studio (Chat Completions API) — local coding chat assistant."
|
||||||
|
|
||||||
provider_instance = "LM Studio (Chat Completions)"
|
model = "local-model"
|
||||||
endpoint = "/v1/chat/completions"
|
|
||||||
model = "local-model"
|
|
||||||
|
|
||||||
tags = ["chat", "lmstudio", "local"]
|
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
schema_version = 1
|
schema_version = 1
|
||||||
|
|
||||||
extends = "OpenAI Responses Base"
|
extends = "LM Studio Responses Base"
|
||||||
name = "LM Studio Responses"
|
name = "LM Studio Responses"
|
||||||
description = "LM Studio (Responses API) — local coding chat assistant."
|
description = "LM Studio (Responses API) — local coding chat assistant."
|
||||||
|
|
||||||
provider_instance = "LM Studio (Responses API)"
|
model = "local-model"
|
||||||
endpoint = "/v1/responses"
|
|
||||||
model = "local-model"
|
|
||||||
|
|
||||||
tags = ["chat", "lmstudio", "responses", "local"]
|
|
||||||
|
|||||||
11
sources/agents/lmstudio_responses_base.toml
Normal file
11
sources/agents/lmstudio_responses_base.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
schema_version = 1
|
||||||
|
|
||||||
|
extends = "OpenAI Responses Base"
|
||||||
|
name = "LM Studio Responses Base"
|
||||||
|
description = "LM Studio Responses API. Abstract — extend it and set model."
|
||||||
|
abstract = true
|
||||||
|
|
||||||
|
provider_instance = "LM Studio (Responses API)"
|
||||||
|
endpoint = "/v1/responses"
|
||||||
|
|
||||||
|
tags = ["chat", "lmstudio", "responses", "local"]
|
||||||
11
sources/agents/mistral_base_chat.toml
Normal file
11
sources/agents/mistral_base_chat.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
schema_version = 1
|
||||||
|
|
||||||
|
extends = "OpenAI Base Chat"
|
||||||
|
name = "Mistral Base Chat"
|
||||||
|
description = "Mistral AI Chat Completions. Abstract — extend it and set model."
|
||||||
|
abstract = true
|
||||||
|
|
||||||
|
provider_instance = "Mistral AI"
|
||||||
|
endpoint = "/v1/chat/completions"
|
||||||
|
|
||||||
|
tags = ["chat", "mistral", "cloud"]
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
schema_version = 1
|
schema_version = 1
|
||||||
|
|
||||||
extends = "OpenAI Base Chat"
|
extends = "Mistral Base Chat"
|
||||||
name = "Mistral Chat"
|
name = "Mistral Chat"
|
||||||
description = "Mistral Large (Chat Completions API) — coding chat assistant."
|
description = "Mistral Large (Chat Completions API) — coding chat assistant."
|
||||||
|
|
||||||
provider_instance = "Mistral AI"
|
model = "mistral-large-latest"
|
||||||
endpoint = "/v1/chat/completions"
|
|
||||||
model = "mistral-large-latest"
|
|
||||||
|
|
||||||
tags = ["chat", "mistral", "cloud"]
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
schema_version = 1
|
schema_version = 1
|
||||||
|
|
||||||
extends = "OpenAI Base Chat"
|
extends = "Mistral Base Chat"
|
||||||
name = "Mistral Medium Chat"
|
name = "Mistral Medium Chat"
|
||||||
description = "Mistral Medium 3.5 (Chat Completions API) — frontier coding/agentic chat."
|
description = "Mistral Medium 3.5 (Chat Completions API) — frontier coding/agentic chat."
|
||||||
|
|
||||||
provider_instance = "Mistral AI"
|
model = "mistral-medium-latest"
|
||||||
endpoint = "/v1/chat/completions"
|
|
||||||
model = "mistral-medium-latest"
|
|
||||||
|
|
||||||
tags = ["chat", "mistral", "medium", "cloud"]
|
tags = ["chat", "mistral", "medium", "cloud"]
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
schema_version = 1
|
schema_version = 1
|
||||||
|
|
||||||
extends = "OpenAI Base Chat"
|
extends = "Mistral Base Chat"
|
||||||
name = "Mistral Reasoning Chat"
|
name = "Mistral Reasoning Chat"
|
||||||
description = "Mistral Magistral Medium — native chain-of-thought reasoning model."
|
description = "Mistral Magistral Medium — native chain-of-thought reasoning model."
|
||||||
|
|
||||||
provider_instance = "Mistral AI"
|
model = "magistral-medium-latest"
|
||||||
endpoint = "/v1/chat/completions"
|
|
||||||
model = "magistral-medium-latest"
|
|
||||||
|
|
||||||
enable_thinking = true
|
enable_thinking = true
|
||||||
tags = ["chat", "mistral", "reasoning", "cloud"]
|
tags = ["chat", "mistral", "reasoning", "cloud"]
|
||||||
|
|||||||
11
sources/agents/ollama_openai_base_chat.toml
Normal file
11
sources/agents/ollama_openai_base_chat.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
schema_version = 1
|
||||||
|
|
||||||
|
extends = "OpenAI Base Chat"
|
||||||
|
name = "Ollama (OpenAI-compatible) Base Chat"
|
||||||
|
description = "Ollama via its OpenAI-compatible Chat Completions endpoint. Abstract — extend it and set model."
|
||||||
|
abstract = true
|
||||||
|
|
||||||
|
provider_instance = "Ollama (OpenAI-compatible)"
|
||||||
|
endpoint = "/v1/chat/completions"
|
||||||
|
|
||||||
|
tags = ["chat", "ollama", "local"]
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
schema_version = 1
|
schema_version = 1
|
||||||
|
|
||||||
extends = "OpenAI Base Chat"
|
extends = "Ollama (OpenAI-compatible) Base Chat"
|
||||||
name = "Ollama (OpenAI-compatible) Chat"
|
name = "Ollama (OpenAI-compatible) Chat"
|
||||||
description = "Ollama via its OpenAI-compatible Chat Completions endpoint — local coding chat assistant."
|
description = "Ollama via its OpenAI-compatible Chat Completions endpoint — local coding chat assistant."
|
||||||
|
|
||||||
provider_instance = "Ollama (OpenAI-compatible)"
|
model = "qwen2.5-coder"
|
||||||
endpoint = "/v1/chat/completions"
|
|
||||||
model = "qwen2.5-coder"
|
|
||||||
|
|
||||||
tags = ["chat", "ollama", "local"]
|
|
||||||
|
|||||||
15
sources/agents/ollama_qwen25_coder_fim.toml
Normal file
15
sources/agents/ollama_qwen25_coder_fim.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
schema_version = 1
|
||||||
|
|
||||||
|
extends = "Ollama FIM Base"
|
||||||
|
name = "Ollama Qwen2.5-Coder FIM"
|
||||||
|
description = "Local Qwen2.5-Coder on Ollama, FIM completion via <|fim_*|> markers."
|
||||||
|
|
||||||
|
model = "qwen2.5-coder"
|
||||||
|
|
||||||
|
tags = ["fim", "ollama", "local", "qwen"]
|
||||||
|
|
||||||
|
[body]
|
||||||
|
prompt = """{% if existsIn(ctx, "suffix") and length(ctx.suffix) > 0 %}{{ tojson("<|fim_prefix|>" + ctx.prefix + "<|fim_suffix|>" + ctx.suffix + "<|fim_middle|>") }}{% else %}{{ tojson("<|fim_prefix|>" + ctx.prefix + "<|fim_middle|>") }}{% endif %}"""
|
||||||
|
|
||||||
|
[body.options]
|
||||||
|
stop = ["<|endoftext|>", "<|fim_prefix|>", "<|fim_suffix|>", "<|fim_middle|>", "<|fim_pad|>", "<|repo_name|>", "<|file_sep|>"]
|
||||||
10
sources/agents/openai_compatible_base_chat.toml
Normal file
10
sources/agents/openai_compatible_base_chat.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
schema_version = 1
|
||||||
|
|
||||||
|
extends = "OpenAI Base Chat"
|
||||||
|
name = "OpenAI Compatible Base Chat"
|
||||||
|
description = "Any OpenAI-compatible Chat Completions endpoint. Abstract — extend it and set model."
|
||||||
|
abstract = true
|
||||||
|
|
||||||
|
provider_instance = "OpenAI Compatible"
|
||||||
|
|
||||||
|
tags = ["chat", "openai", "compatible"]
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
schema_version = 1
|
schema_version = 1
|
||||||
|
|
||||||
extends = "OpenAI Base Chat"
|
extends = "OpenAI Compatible Base Chat"
|
||||||
name = "OpenAI Compatible Chat"
|
name = "OpenAI Compatible Chat"
|
||||||
description = "Any OpenAI-compatible Chat Completions endpoint — set the model to match your server."
|
description = "Any OpenAI-compatible Chat Completions endpoint — set the model to match your server."
|
||||||
|
|
||||||
provider_instance = "OpenAI Compatible"
|
model = "default"
|
||||||
model = "default"
|
|
||||||
|
|
||||||
tags = ["chat", "openai", "compatible"]
|
|
||||||
|
|||||||
11
sources/agents/openrouter_base_chat.toml
Normal file
11
sources/agents/openrouter_base_chat.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
schema_version = 1
|
||||||
|
|
||||||
|
extends = "OpenAI Base Chat"
|
||||||
|
name = "OpenRouter Base Chat"
|
||||||
|
description = "OpenRouter (OpenAI-compatible Chat Completions). Abstract — extend it and set model."
|
||||||
|
abstract = true
|
||||||
|
|
||||||
|
provider_instance = "OpenRouter"
|
||||||
|
endpoint = "/chat/completions"
|
||||||
|
|
||||||
|
tags = ["chat", "openrouter", "cloud"]
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
schema_version = 1
|
schema_version = 1
|
||||||
|
|
||||||
extends = "OpenAI Base Chat"
|
extends = "OpenRouter Base Chat"
|
||||||
name = "OpenRouter Chat"
|
name = "OpenRouter Chat"
|
||||||
description = "OpenRouter (OpenAI-compatible Chat Completions) — coding chat assistant."
|
description = "OpenRouter (OpenAI-compatible Chat Completions) — coding chat assistant."
|
||||||
|
|
||||||
provider_instance = "OpenRouter"
|
model = "openai/gpt-4o"
|
||||||
endpoint = "/chat/completions"
|
|
||||||
model = "openai/gpt-4o"
|
|
||||||
|
|
||||||
tags = ["chat", "openrouter", "cloud"]
|
|
||||||
|
|||||||
@@ -147,10 +147,7 @@ AgentRowCard::AgentRowCard(const AgentConfig &cfg, QWidget *parent)
|
|||||||
|
|
||||||
Pill *sourcePill = nullptr;
|
Pill *sourcePill = nullptr;
|
||||||
if (cfg.isUserSource()) {
|
if (cfg.isUserSource()) {
|
||||||
sourcePill = new Pill(
|
sourcePill = new Pill(Pill::User, Tr::tr("User"), this);
|
||||||
Pill::User,
|
|
||||||
cfg.overridesBundled ? Tr::tr("Override") : Tr::tr("User"),
|
|
||||||
this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto *description = new QLabel(this);
|
auto *description = new QLabel(this);
|
||||||
|
|||||||
@@ -296,9 +296,7 @@ void AgentSlotWidget::setAgentConfig(const AgentConfig &cfg)
|
|||||||
m_name->setText(cfg.name);
|
m_name->setText(cfg.name);
|
||||||
|
|
||||||
if (cfg.isUserSource()) {
|
if (cfg.isUserSource()) {
|
||||||
m_sourcePill->setText(cfg.overridesBundled
|
m_sourcePill->setText(Tr::tr("User"));
|
||||||
? Tr::tr("User overrides bundled")
|
|
||||||
: Tr::tr("User"));
|
|
||||||
m_sourcePill->show();
|
m_sourcePill->show();
|
||||||
} else {
|
} else {
|
||||||
m_sourcePill->hide();
|
m_sourcePill->hide();
|
||||||
|
|||||||
@@ -95,10 +95,10 @@ void fillMissingFromDefaults(PipelineRosters &r, const toml::table §ion)
|
|||||||
PipelineRosters PipelineRosters::defaults()
|
PipelineRosters PipelineRosters::defaults()
|
||||||
{
|
{
|
||||||
PipelineRosters r;
|
PipelineRosters r;
|
||||||
r.codeCompletion = {QStringLiteral("Ollama Qwen2.5-Coder Completion")};
|
r.codeCompletion = {QStringLiteral("Ollama Qwen2.5-Coder FIM")};
|
||||||
r.chatAssistant = {QStringLiteral("Ollama Chat")};
|
r.chatAssistant = {QStringLiteral("Ollama (OpenAI-compatible) Chat")};
|
||||||
r.chatCompression = {QStringLiteral("Ollama Compression")};
|
r.chatCompression = {QStringLiteral("Ollama (OpenAI-compatible) Chat")};
|
||||||
r.quickRefactor = {QStringLiteral("Ollama Quick Refactor")};
|
r.quickRefactor = {QStringLiteral("Ollama (OpenAI-compatible) Chat")};
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
184
test/AgentLoaderTest.cpp
Normal file
184
test/AgentLoaderTest.cpp
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QTemporaryDir>
|
||||||
|
|
||||||
|
#include <AgentConfig.hpp>
|
||||||
|
#include <AgentLoader.hpp>
|
||||||
|
|
||||||
|
using QodeAssist::AgentConfig;
|
||||||
|
using QodeAssist::Agents::AgentLoader;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void writeFile(const QString &dir, const QString &name, const QByteArray &contents)
|
||||||
|
{
|
||||||
|
QFile f(dir + QLatin1Char('/') + name);
|
||||||
|
ASSERT_TRUE(f.open(QIODevice::WriteOnly | QIODevice::Text));
|
||||||
|
f.write(contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray minimalAgent(const QByteArray &name, const QByteArray &extra = {})
|
||||||
|
{
|
||||||
|
return "name = \"" + name + "\"\n"
|
||||||
|
"provider_instance = \"P\"\n"
|
||||||
|
"model = \"m\"\n"
|
||||||
|
"endpoint = \"/e\"\n"
|
||||||
|
+ extra +
|
||||||
|
"[body]\n"
|
||||||
|
"stream = true\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
const AgentConfig *findConfig(const AgentLoader::LoadResult &result, const QString &name)
|
||||||
|
{
|
||||||
|
for (const auto &cfg : result.configs) {
|
||||||
|
if (cfg.name == name)
|
||||||
|
return &cfg;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool anyContains(const QStringList &list, const QString &needle)
|
||||||
|
{
|
||||||
|
for (const QString &s : list) {
|
||||||
|
if (s.contains(needle))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(AgentLoaderTest, WarnsOnUnknownTopLevelAndMatchKeys)
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
ASSERT_TRUE(dir.isValid());
|
||||||
|
writeFile(dir.path(), "a.toml",
|
||||||
|
minimalAgent("A",
|
||||||
|
"enable_thinkin = true\n"
|
||||||
|
"[match]\n"
|
||||||
|
"file_pattern = [\"*.cpp\"]\n"));
|
||||||
|
|
||||||
|
const auto result = AgentLoader::load(QString(), dir.path());
|
||||||
|
EXPECT_TRUE(result.errors.isEmpty()) << result.errors.join("; ").toStdString();
|
||||||
|
EXPECT_TRUE(anyContains(result.warnings, QStringLiteral("enable_thinkin")));
|
||||||
|
EXPECT_TRUE(anyContains(result.warnings, QStringLiteral("match.file_pattern")));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AgentLoaderTest, WarnsOnDuplicateNameInSameLayer)
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
ASSERT_TRUE(dir.isValid());
|
||||||
|
writeFile(dir.path(), "first.toml", minimalAgent("Dup"));
|
||||||
|
writeFile(dir.path(), "second.toml", minimalAgent("Dup"));
|
||||||
|
|
||||||
|
const auto result = AgentLoader::load(QString(), dir.path());
|
||||||
|
EXPECT_TRUE(anyContains(result.warnings, QStringLiteral("defined in both")));
|
||||||
|
const AgentConfig *cfg = findConfig(result, QStringLiteral("Dup"));
|
||||||
|
ASSERT_NE(cfg, nullptr);
|
||||||
|
EXPECT_TRUE(cfg->sourcePath.endsWith(QStringLiteral("second.toml")));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AgentLoaderTest, UserAgentCollidingWithBundledNameIsRejected)
|
||||||
|
{
|
||||||
|
QTemporaryDir bundled;
|
||||||
|
QTemporaryDir user;
|
||||||
|
ASSERT_TRUE(bundled.isValid());
|
||||||
|
ASSERT_TRUE(user.isValid());
|
||||||
|
writeFile(bundled.path(), "a.toml", minimalAgent("A", "description = \"base\"\n"));
|
||||||
|
writeFile(user.path(), "a.toml", minimalAgent("A", "description = \"mine\"\n"));
|
||||||
|
|
||||||
|
const auto result = AgentLoader::load(bundled.path(), user.path());
|
||||||
|
EXPECT_TRUE(anyContains(result.errors, QStringLiteral("cannot be replaced")));
|
||||||
|
const AgentConfig *cfg = findConfig(result, QStringLiteral("A"));
|
||||||
|
ASSERT_NE(cfg, nullptr);
|
||||||
|
EXPECT_EQ(cfg->description, QStringLiteral("base"));
|
||||||
|
EXPECT_FALSE(cfg->isUserSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AgentLoaderTest, HiddenIsNotInherited)
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
ASSERT_TRUE(dir.isValid());
|
||||||
|
writeFile(dir.path(), "parent.toml", minimalAgent("Parent", "hidden = true\n"));
|
||||||
|
writeFile(dir.path(), "child.toml", minimalAgent("Child", "extends = \"Parent\"\n"));
|
||||||
|
|
||||||
|
const auto result = AgentLoader::load(QString(), dir.path());
|
||||||
|
EXPECT_TRUE(result.errors.isEmpty()) << result.errors.join("; ").toStdString();
|
||||||
|
const AgentConfig *parent = findConfig(result, QStringLiteral("Parent"));
|
||||||
|
const AgentConfig *child = findConfig(result, QStringLiteral("Child"));
|
||||||
|
ASSERT_NE(parent, nullptr);
|
||||||
|
ASSERT_NE(child, nullptr);
|
||||||
|
EXPECT_TRUE(parent->hidden);
|
||||||
|
EXPECT_FALSE(child->hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AgentLoaderTest, UserAgentExtendsBundledProviderBase)
|
||||||
|
{
|
||||||
|
QTemporaryDir bundled;
|
||||||
|
QTemporaryDir user;
|
||||||
|
ASSERT_TRUE(bundled.isValid());
|
||||||
|
ASSERT_TRUE(user.isValid());
|
||||||
|
writeFile(bundled.path(), "base.toml",
|
||||||
|
"name = \"Provider Base\"\n"
|
||||||
|
"abstract = true\n"
|
||||||
|
"provider_instance = \"P\"\n"
|
||||||
|
"endpoint = \"/e\"\n"
|
||||||
|
"[body]\n"
|
||||||
|
"stream = true\n");
|
||||||
|
writeFile(bundled.path(), "a.toml",
|
||||||
|
"name = \"A\"\n"
|
||||||
|
"extends = \"Provider Base\"\n"
|
||||||
|
"model = \"stock-model\"\n");
|
||||||
|
writeFile(user.path(), "mine.toml",
|
||||||
|
"name = \"My A\"\n"
|
||||||
|
"extends = \"Provider Base\"\n"
|
||||||
|
"model = \"my-model\"\n"
|
||||||
|
"[body]\n"
|
||||||
|
"temperature = 0.2\n");
|
||||||
|
|
||||||
|
const auto result = AgentLoader::load(bundled.path(), user.path());
|
||||||
|
EXPECT_TRUE(result.errors.isEmpty()) << result.errors.join("; ").toStdString();
|
||||||
|
const AgentConfig *stock = findConfig(result, QStringLiteral("A"));
|
||||||
|
ASSERT_NE(stock, nullptr);
|
||||||
|
EXPECT_EQ(stock->model, QStringLiteral("stock-model"));
|
||||||
|
const AgentConfig *mine = findConfig(result, QStringLiteral("My A"));
|
||||||
|
ASSERT_NE(mine, nullptr);
|
||||||
|
EXPECT_EQ(mine->model, QStringLiteral("my-model"));
|
||||||
|
EXPECT_EQ(mine->providerInstance, QStringLiteral("P"));
|
||||||
|
EXPECT_TRUE(mine->body.contains(QStringLiteral("stream")));
|
||||||
|
EXPECT_TRUE(mine->body.contains(QStringLiteral("temperature")));
|
||||||
|
EXPECT_TRUE(mine->isUserSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AgentLoaderTest, ExtendsUnknownParentErrorNamesChild)
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
ASSERT_TRUE(dir.isValid());
|
||||||
|
writeFile(dir.path(), "child.toml", minimalAgent("Child", "extends = \"NoSuchBase\"\n"));
|
||||||
|
|
||||||
|
const auto result = AgentLoader::load(QString(), dir.path());
|
||||||
|
EXPECT_TRUE(anyContains(result.errors, QStringLiteral("'Child' extends unknown agent 'NoSuchBase'")));
|
||||||
|
EXPECT_EQ(findConfig(result, QStringLiteral("Child")), nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AgentLoaderTest, ParseFileReportsWarningsForOwnFileOnly)
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
ASSERT_TRUE(dir.isValid());
|
||||||
|
writeFile(dir.path(), "other.toml", minimalAgent("Other", "bogus_key = 1\n"));
|
||||||
|
writeFile(dir.path(), "target.toml", minimalAgent("Target", "another_bogus = 2\n"));
|
||||||
|
|
||||||
|
QString error;
|
||||||
|
QStringList warnings;
|
||||||
|
const auto cfg = AgentLoader::parseFile(
|
||||||
|
dir.path() + QStringLiteral("/target.toml"), QString(), &error, &warnings);
|
||||||
|
ASSERT_TRUE(cfg.has_value()) << error.toStdString();
|
||||||
|
EXPECT_TRUE(anyContains(warnings, QStringLiteral("another_bogus")));
|
||||||
|
EXPECT_FALSE(anyContains(warnings, QStringLiteral("bogus_key")));
|
||||||
|
}
|
||||||
@@ -23,6 +23,10 @@ TEST(BundledAgentsTest, AllBundledAgentsLoadResolveAndRender)
|
|||||||
<< "bundled agent load errors: "
|
<< "bundled agent load errors: "
|
||||||
<< result.errors.join(QStringLiteral("; ")).toStdString();
|
<< result.errors.join(QStringLiteral("; ")).toStdString();
|
||||||
|
|
||||||
|
EXPECT_TRUE(result.warnings.isEmpty())
|
||||||
|
<< "bundled agent load warnings: "
|
||||||
|
<< result.warnings.join(QStringLiteral("; ")).toStdString();
|
||||||
|
|
||||||
ASSERT_FALSE(result.configs.empty()) << "no bundled agents were loaded from :/agents";
|
ASSERT_FALSE(result.configs.empty()) << "no bundled agents were loaded from :/agents";
|
||||||
|
|
||||||
for (const auto &cfg : result.configs) {
|
for (const auto &cfg : result.configs) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ add_executable(QodeAssistTest
|
|||||||
JsonPromptTemplateTest.cpp
|
JsonPromptTemplateTest.cpp
|
||||||
ResponseRouterTest.cpp
|
ResponseRouterTest.cpp
|
||||||
BundledAgentsTest.cpp
|
BundledAgentsTest.cpp
|
||||||
|
AgentLoaderTest.cpp
|
||||||
# LLMClientInterfaceTests.cpp
|
# LLMClientInterfaceTests.cpp
|
||||||
unittest_main.cpp
|
unittest_main.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user