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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,18 @@ Secrets are referenced as `${vault:NAME}` in config and `{{secret:NAME}}` in com

<details>

<summary><strong>Deep research</strong> — Multi-step web research with cited reports</summary>

- `deep_research` tool: decompose the question → search → read sources → follow up on gaps → synthesize
- Structured, cited report published as a themed web artifact; brief summary + link in chat
- Fast/cheap model for sub-calls, main agent model (or a dedicated one) for synthesis
- Configurable depth, max sources, and a hard per-run token budget
- Streams progress to the chat; cancellable with `/stop`

</details>

<details>

<summary><strong>Image generation</strong> — Visual answers</summary>

- Optional `generate_image` tool (OpenRouter, fal.ai, or OpenAI)
Expand Down Expand Up @@ -320,6 +332,7 @@ humux/
│ ├── artifacts.py Web artifact serving (sandboxed)
│ ├── coding.py Confined workspace file tools
│ ├── compaction.py Conversation compaction for session history
│ ├── deep_research.py Multi-step web research pipeline
│ ├── github_app.py GitHub App JWT minting + installation tokens
│ ├── history.py Conversation history persistence
│ ├── imagegen.py Image generation with budget caps
Expand Down
1 change: 1 addition & 0 deletions docs/content/docs/admin-ui.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Edit agent settings without touching config files:
Enable optional tools. When a tool is enabled it is authenticated and advertised to the assistant in its system prompt; when disabled it stays hidden, keeping the prompt lean. Each tool honours the same per-agent whitelist and permission rules as any other tool.

- **Web search** — let the agent look things up online via the `web_search` tool (currently Tavily). Toggle enabled, paste the API key (stored in the [vault](/docs/secrets)), pick max results, and **Test connection**. When disabled the tool is hidden from the model entirely.
- **Deep research** — autonomous multi-step web research via the `deep_research` tool: the question is decomposed into sub-questions, each is searched (using the configured Web search backend) and its top sources read, gaps trigger follow-up searches, and the result is a structured, cited report — published as a themed [web artifact](/docs/extending) when the workspace is enabled, so the chat reply stays a brief summary plus a link. Configure the **sub-call model** (fast/cheap; planning, extraction, gap analysis), the **synthesis model** (blank = the main agent model), and the cost guardrails: **depth** (search-read-iterate cycles), **max sources**, and a hard per-run **token budget**. The tool posts progress updates to the chat as it works and is cancellable with `/stop`. Needs Web search configured.
- **GitHub CLI (`gh`)** — let the agent query and act on GitHub (issues, PRs, repos, `gh api`, search). Authenticate with a **Personal Access Token** (simple; actions appear as you) or a **[GitHub App](/docs/extending#authenticating-pat-or-github-app)** (recommended — its own bot identity `<app>[bot]`, fine-grained per-repo permissions, and its own rate limit). For a PAT, paste a [token](https://github.com/settings/tokens); for an App, paste the **App ID**, **Installation ID**, and **private key** (PEM — pasting works on mobile, stored in the vault). Either way it is injected as `GH_TOKEN`; the App takes precedence, with the PAT as a fallback. Read operations (`list`/`view`/`status`/`api`/`search`) run without asking; creating issues, PRs, or releases ask for confirmation first. **Test connection** verifies the credentials and shows the authenticated identity (and, for an App, the accessible repos). The whole setup is completable at phone width. In session history mode, a newly enabled tool is advertised starting from the next `/new` conversation.

### Skills
Expand Down
17 changes: 17 additions & 0 deletions docs/content/docs/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,23 @@ single config change with no code modification.
exposes it at `http://searxng:8080`. Pick the provider and test reachability
from the admin UI (Tools → Web search) or the setup wizard.

#### `tools.deep_research`

The `deep_research` tool (Tools → Deep research in the admin UI): autonomous
multi-step research on top of `search`. Off by default; needs web search
configured.

- `provider` / `model` — the fast/cheap model for sub-calls (planning,
per-source extraction, gap analysis). Default `deepseek` /
`deepseek-v4-flash`.
- `synthesis_provider` / `synthesis_model` — the model that writes the final
report. Blank (default) = the main agent model.
- `depth` (default `2`) — search-read-iterate cycles; also the ceiling when the
agent picks its own per-call value.
- `max_sources` (default `10`) — pages read per run; also a ceiling.
- `token_budget` (default `300000`) — hard token ceiling per run; when
exceeded the loop stops early and synthesizes from what was gathered.

#### `vision`

Vision fallback. When the active model can't read images, image attachments are routed to `model` (on `provider`, reusing that provider's `agent` API key) for a text description / OCR, injected in place of the image so non-multimodal models can still "see". Off by default; engages only when the active model lacks native vision. The admin LLM tab and the setup wizard surface a one-tap enable prompt when you select a model that can't read images.
Expand Down
1 change: 1 addition & 0 deletions docs/content/docs/permissions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ humux ships with sensible defaults:
"bash:pwd*" "bash:whoami*" "bash:id*" # cat/ls/wc/df/…
"bash:wc*" "bash:df*" "bash:du*" # (see note below)
"web_search" # Web searches
"deep_research" # Multi-step web research
"set_reaction" # Emoji reactions (Telegram)
```

Expand Down
21 changes: 21 additions & 0 deletions humux/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2145,6 +2145,17 @@ async def partial_tools() -> HTMLResponse:
ig_daily = await config_store.get("tools.imagegen.daily_budget") or "0"
ig_monthly = await config_store.get("tools.imagegen.monthly_budget") or "0"

# Deep research (issue #293). One prefix fetch; fall back to the
# pydantic defaults so they live in config.py only.
from core.config import DeepResearchToolConfig

dr_defaults = DeepResearchToolConfig()
dr_stored = await config_store.get_many("tools.deep_research.")

def dr_get(field: str) -> str:
stored = dr_stored.get(f"tools.deep_research.{field}")
return stored if stored is not None else str(getattr(dr_defaults, field)).lower()

# WhatsApp tool (#97) — enable flag + wacli link status for the badge.
wa_enabled = await config_store.get("tools.whatsapp.enabled")
wa_enabled = wa_enabled if wa_enabled is not None else "false"
Expand Down Expand Up @@ -2192,6 +2203,14 @@ async def partial_tools() -> HTMLResponse:
imagegen_key_vaulted=ig_key_vaulted,
imagegen_daily_budget=ig_daily,
imagegen_monthly_budget=ig_monthly,
deep_research_enabled=dr_get("enabled"),
deep_research_provider=dr_get("provider"),
deep_research_model=dr_get("model"),
deep_research_synth_provider=dr_get("synthesis_provider"),
deep_research_synth_model=dr_get("synthesis_model"),
deep_research_depth=dr_get("depth"),
deep_research_max_sources=dr_get("max_sources"),
deep_research_token_budget=dr_get("token_budget"),
search_enabled=search_enabled,
search_provider=search_provider,
search_api_key="" if search_vaulted else search_api_key,
Expand Down Expand Up @@ -5194,6 +5213,7 @@ def _config_requires_restart(values: dict) -> bool:
"search_contacts",
"create_contact",
"web_search",
"deep_research",
"manage_jobs",
"spawn_subagent",
"generate_image",
Expand All @@ -5220,6 +5240,7 @@ def _config_requires_restart(values: dict) -> bool:
"search_contacts": "Look up people in a bound address book.",
"create_contact": "Add a contact to a bound address book.",
"web_search": "Search the web (needs the Web search tool enabled).",
"deep_research": "Multi-step web research with a synthesized, cited report.",
"manage_jobs": "Schedule, list and cancel the agent's own jobs.",
"spawn_subagent": "Delegate a scoped subtask to a child agent.",
"generate_image": "Generate images and send them as photos.",
Expand Down
122 changes: 122 additions & 0 deletions humux/api/templates/partials/tools.html
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,91 @@ <h3 class="text-sm text-accent">Web search</h3>
</template>
</div>

{# Deep research (issue #293) #}
<div class="card" x-data="deepResearchTool(
{{ deep_research_enabled|default('false', true)|tojson|forceescape }},
{{ deep_research_provider|default('deepseek', true)|tojson|forceescape }},
{{ deep_research_model|default('deepseek-v4-flash', true)|tojson|forceescape }},
{{ deep_research_synth_provider|default('', true)|tojson|forceescape }},
{{ deep_research_synth_model|default('', true)|tojson|forceescape }},
{{ deep_research_depth|default('2', true)|tojson|forceescape }},
{{ deep_research_max_sources|default('10', true)|tojson|forceescape }},
{{ deep_research_token_budget|default('300000', true)|tojson|forceescape }}
)">
<div class="flex items-center justify-between gap-3 mb-1">
<h3 class="text-sm text-accent">Deep research</h3>
<div class="flex items-center gap-2">
<label class="label mb-0" for="tools-dr-enabled">Enabled</label>
<input type="checkbox" x-model="enabled" :checked="enabled === 'true' || enabled === true" id="tools-dr-enabled">
</div>
</div>
<p class="text-muted text-xs mb-4">
Autonomous multi-step web research (the <code>deep_research</code> tool):
the question is split into sub-questions, each is searched and its sources
read, gaps trigger follow-up searches, and the result is a structured, cited
report — published as a web artifact when the workspace is enabled. Needs the
<strong>Web search</strong> tool configured. Cancel a running research with
<code>/stop</code> (a background subagent run: from its cancel button in
<strong>Jobs</strong>). Assign it per agent in the <strong>Agents</strong> editor.
</p>
<div class="space-y-3" x-show="enabled === 'true' || enabled === true">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="label" for="tools-dr-provider">Sub-call provider</label>
<select class="select input-sm" style="max-width:100%" x-model="provider" id="tools-dr-provider">
<template x-for="p in window.LLM_PROVIDERS" :key="p.value">
<option :value="p.value" x-text="p.label" :selected="p.value === provider"></option>
</template>
</select>
<p class="text-muted text-xs mt-1">Fast/cheap model for planning, extraction and gap analysis. Uses that provider's configured API key.</p>
</div>
<div>
<label class="label" for="tools-dr-model">Sub-call model</label>
<input type="text" class="input-sm" style="max-width:100%" x-model="model"
placeholder="deepseek-v4-flash" id="tools-dr-model">
</div>
<div>
<label class="label" for="tools-dr-synth-provider">Synthesis provider</label>
<select class="select input-sm" style="max-width:100%" x-model="synthProvider" id="tools-dr-synth-provider">
<option value="">Main agent model</option>
<template x-for="p in window.LLM_PROVIDERS" :key="p.value">
<option :value="p.value" x-text="p.label" :selected="p.value === synthProvider"></option>
</template>
</select>
<p class="text-muted text-xs mt-1">Writes the final report. Blank = the model of the agent that ran the tool.</p>
</div>
<div x-show="synthProvider">
<label class="label" for="tools-dr-synth-model">Synthesis model</label>
<input type="text" class="input-sm" style="max-width:100%" x-model="synthModel"
placeholder="claude-sonnet-5" id="tools-dr-synth-model">
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<label class="label" for="tools-dr-depth">Depth</label>
<input type="number" min="1" max="3" class="input-sm" style="max-width:120px" x-model="depth" id="tools-dr-depth">
<p class="text-muted text-xs mt-1">Search-read-iterate cycles (1-3). Also the cap when the agent picks its own.</p>
</div>
<div>
<label class="label" for="tools-dr-max-sources">Max sources</label>
<input type="number" min="1" class="input-sm" style="max-width:120px" x-model="maxSources" id="tools-dr-max-sources">
<p class="text-muted text-xs mt-1">Pages read per run — a cap too.</p>
</div>
<div>
<label class="label" for="tools-dr-token-budget">Token budget</label>
<input type="number" min="1000" class="input-sm" style="max-width:160px" x-model="tokenBudget" id="tools-dr-token-budget">
<p class="text-muted text-xs mt-1">Hard token ceiling per run.</p>
</div>
</div>
</div>
<div class="flex items-center gap-2 mt-4">
<button class="btn-primary btn-sm" @click="withLoading($el, () => save())">Save</button>
</div>
<template x-if="result">
<div :class="resultOk ? 'alert-success' : 'alert-error'" class="mt-2" x-text="result"></div>
</template>
</div>

{# WhatsApp via the local wacli CLI (issue #97) — a tool, not a channel #}
<div class="card" x-data="whatsappTool(
{{ whatsapp_enabled|default('false', true)|tojson|forceescape }},
Expand Down Expand Up @@ -757,6 +842,43 @@ <h4 class="text-xs text-muted mb-2">Per-domain action rules</h4>
};
}

function deepResearchTool(enabled, provider, model, synthProvider, synthModel, depth, maxSources, tokenBudget) {
return {
enabled: enabled,
provider: provider || 'deepseek',
model: model || 'deepseek-v4-flash',
synthProvider: synthProvider || '',
synthModel: synthModel || '',
depth: depth || '2',
maxSources: maxSources || '10',
tokenBudget: tokenBudget || '300000',
result: '', resultOk: false,
save() {
const vals = {
'tools.deep_research.enabled': String(this.enabled === true || this.enabled === 'true'),
'tools.deep_research.provider': String(this.provider || 'deepseek').trim(),
'tools.deep_research.model': String(this.model || '').trim(),
'tools.deep_research.synthesis_provider': String(this.synthProvider || '').trim(),
'tools.deep_research.synthesis_model': String(this.synthModel || '').trim(),
'tools.deep_research.depth': String(parseInt(this.depth) || 2),
'tools.deep_research.max_sources': String(parseInt(this.maxSources) || 10),
'tools.deep_research.token_budget': String(parseInt(this.tokenBudget) || 300000)
};
return fetch('/config', {
method: 'PATCH',
headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + (localStorage.getItem('admin_api_key') || '')},
body: JSON.stringify({values: vals})
})
.then(r => { this.resultOk = r.ok; return r.json(); })
.then(d => {
this.result = this.resultOk ? 'Deep research settings saved' : (d.detail || 'Error');
if (this.resultOk && window.showToast) { window.showToast('Deep research settings saved'); }
})
.catch(e => { this.resultOk = false; this.result = e.message; });
}
};
}

function imagegenTab(enabled, provider, model, apiKey, keyVaulted, daily, monthly) {
const truthy = (v) => v === true || v === 'true';
const DEFAULTS = { openrouter: 'google/gemini-2.5-flash-image', fal: 'fal-ai/flux/schnell', openai: 'gpt-image-1-mini' };
Expand Down
Loading
Loading