Repository files navigation

dispatch

License: MIT

Self-hosted AI research agent platform. Define agents for topics you care about, schedule them as dispatches, and get synthesized digests — no manual searching. Runs on your machine or server. Bring your own LLM (Ollama, OpenAI, Anthropic, Fireworks).

Read Digest

What you get

  • Web UI to browse, schedule, and read agent output — no config files required
  • Daily digest compiled from the most recent run of each active agent
  • Multi-track pipelines that fan out searches across independent topic tracks in parallel

Quick Start

# 1. Create and activate a virtual environment
python -m venv dispatch-env
source dispatch-env/bin/activate
# 2. Install dependencies
pip install -r requirements.txt
# 3. Configure environment
cp .env.example .env
# Set SERVER_API_KEY if the server will be network-accessible (see .env.example)# Everything else — provider, model, API keys, location — can be set in the Settings UI# 4. Pull a model (Ollama only — skip if using a cloud provider)
ollama pull qwen3
# 5. Start the server
python main.py --serve # http://localhost:8000

Open the web UI, go to Library, create your first agent, then click Schedule to activate it.

How agents work

There are two distinct concepts: agents and dispatches.

  • Agent — what an agent is: its name, goal, sources, search settings, and color. Agents live in the database and are the source of truth for behavior.
  • Dispatch — an agent paired with a user prompt and a schedule. One agent can have multiple dispatches (e.g., the news agent running daily at 7am with a general prompt and again on Fridays with a weekly-recap prompt).

No agents are active by default. Create agents in the Library tab, then activate them by creating a dispatch.

Library tab — create and browse agents
↓
Schedule button → creates a dispatch (agent + prompt + schedule)
↓
Active tab — running scheduled dispatches

How it works

Each agent run uses a multi-phase micro-agent pipeline. Each phase is a small, focused LLM call with a constrained tool budget. The orchestrator (Python) manages state between phases; no single model call holds the full context.

query
→ Planner (LLM) — generate N search queries (or skip if direct_sources set)
→ Searcher (API) — fire all queries in parallel, collect results
→ Validator (LLM) — coverage sufficient? retry with new queries if not
→ Synthesizer (LLM) — format final answer from research buffer

Optional phases (enabled per-agent via config):

  • Link validator — HEAD-checks result URLs, drops dead ones before synthesis
  • Ranker + Fetcher — LLM selects top URLs → parallel web_fetch → per-page summarizer

Adding a new agent

Option 1 — Web UI

Configure Agent

Library tab → New agent → fill in name, goal, sources, and color → Save. The agent immediately appears in the Library and can be scheduled.

Option 2 — YAML

Drop .yaml files into your agents directory (defaults to ./agents, override with DISPATCH_AGENTS_DIR in .env). No agents folder is included in the repo — create it and add your own files.

Create agents/my-agent.yaml:

name: my-agentlabel: My Agentdescription: What this agent covers.color: 175# hue 0–359; omit to auto-assign on first seedgoal_template: "Find recent {query}. Focus on X, Y, Z."output_template: "Return a bulleted digest with source links."sources:
- example.com
- anothersource.orgtool_bindings:
- web_searchplanner_query_count: 5freshness: w # d=24h w=7d m=30d y=1y (omit = any date)

Restart the server — the agent appears in the Library tab. Click Schedule to activate it.

Option 3 — Import YAML

Library tab → Import or POST /api/agents/import with a YAML body. Useful for migrating agents between instances.

YAML field reference

FieldTypeDefaultDescription
namestringrequiredUnique slug — used as the agent identifier everywhere
labelstring""Display name shown in pills and headers
descriptionstring""Short summary shown in the Library table
colorintegerrandomHue (0–359) for the color pill. Auto-assigned on first seed if omitted; never overwritten by re-seeding
goal_templatestringrequiredLLM prompt for what to find. {query} → user input, {today} → YYYY-MM-DD
output_templatestring""Instructions for how the synthesizer should format the response
sourceslist[]Domains to bias planner queries toward (e.g. sfchronicle.com)
direct_sourceslist[]URLs fetched directly, bypassing the planner. {today} substitution supported
sub_querieslistnullMulti-track pipelines — each track runs an isolated search pass (see Pipeline modes)
tool_bindingslist["web_search"]Tools available to this agent. Empty list = all tools
freshnessstringnullDate filter: dwmy
planner_query_countinteger5Number of search queries the planner generates
max_validator_loopsinteger1How many times the validator can request additional searches
validate_linksboolfalseHEAD-check each result URL and drop dead ones before synthesis
fetch_contentboolfalseRank URLs by relevance, fetch top 5 full pages, summarize per page

Pipeline modes

The pipeline is selected automatically based on your config. All modes end with a Validator loop and a Synthesizer call.

1. Search (default)

# no direct_sourcestool_bindings: [web_search]

LLM planner generates planner_query_count queries → parallel web search across all providers → validate → synthesize.

Best for news, research, any topic where broad query coverage helps. sources narrows the planner's focus toward specific domains without locking it to them.

2. Search + deep read

tool_bindings: [web_search, web_fetch]fetch_content: true

Same as search, but after collecting results the pipeline ranks all URLs by relevance, fetches the top 5 full pages, runs a per-page summarizer, and uses the extracted facts for synthesis instead of raw snippets.

Best for agents where the article body matters — detailed reporting, analysis, documentation. Slower (~2× more LLM calls) but higher-quality output.

3. Direct feed

direct_sources:
- https://example.com/feed?date={today}
- https://api.example.com/poststool_bindings: [web_fetch] # required — or omit tool_bindings entirely

Skips the LLM planner entirely. Each URL is fetched directly (with {today} substituted as YYYY-MM-DD). Results go straight into the research buffer.

Best for RSS feeds, known API endpoints, or structured data pages updated on a schedule. Note: tool_bindings must include web_fetch or be empty — the default ["web_search"] will silently skip direct URLs.

4. Domain-biased search

sources:
- sfchronicle.com
- missionlocal.org

The LLM planner is given sources as a hint — it generates queries biased toward those domains but still searches the open web. Not a hard filter.

Best for agents that should prefer specific publications without being locked to them.

5. Multi-track

sub_queries:
- label: researchgoal: "Find recent ML papers about {query}"sources: [arxiv.org, paperswithcode.com]query_count: 3freshness: w
- label: industrygoal: "Find business news about {query} in AI"sources: [techcrunch.com, venturebeat.com]query_count: 3

N independent search pipelines run in parallel, each with its own goal, sources, freshness, and query count. Results are aggregated into a final synthesizer call.

Best for agents covering multiple distinct topics that benefit from isolated query sets (e.g., AI papers vs industry news vs tooling). Note: fetch_content and direct_sources are not inherited by sub-pipelines — each track is search-only.

Options that apply across all modes

validate_links — HEAD-checks every result URL and drops dead ones before synthesis. Adds a few seconds, improves source quality.

max_validator_loops — the validator judges whether the research buffer is sufficient. If not, it generates retry queries and reruns the search phase. Default 1 means one pass (no retry). Set to 23 for agents where completeness matters more than speed.

LLM providers

Configure via the Settings UI (gear icon) or set in .env. Cloud providers automatically use larger context windows and higher parallelism than local Ollama.

ProviderPROVIDER=Recommended models
Ollama (default)ollamaqwen3 / qwen3:1.7b (fast)
OpenAIopenaigpt-4.1 / gpt-4.1-mini (fast)
Anthropicanthropicclaude-sonnet-4-6 / claude-haiku-4-5-20251001 (fast)
Fireworksfireworksllama-v3p3-70b-instruct / llama-v3p1-8b-instruct (fast)

Set MODEL for the primary model and optionally FAST_MODEL for a smaller model used in the Planner/Validator phases.

Search providers

Web search fans out across multiple providers in parallel, merges and deduplicates results. DuckDuckGo works out of the box — no key required. Add any of the following free-tier providers to expand coverage:

ProviderKeyFree tierNotes
Ollama CloudOLLAMA_API_KEYollama.comRecommended; primary fan-out provider
Brave SearchBRAVE_API_KEY2K queries/month — brave.com/search/apiGood general coverage
Jina SearchJINA_API_KEYjina.aiAlso enables Jina Reader for page fetching; site: queries excluded
DuckDuckGononealways-onBaseline; 60s circuit-breaker, auto-recovers

Page fetching uses requests with automatic fallback to Jina Reader (r.jina.ai) when a page returns thin content — catches JavaScript-rendered shells transparently. Set WEB_FETCH_PROVIDER=jina to always use Jina.

Environment variables

Required (set in .env before first run)

These are read at startup before the UI is accessible and cannot be changed via the Settings UI.

VariableDefaultDescription
SERVER_API_KEYunsetBearer token for the HTTP API. Set this if the server is network-accessible — without it anyone who can reach the host can trigger runs and read your data. Not needed for localhost use.
SERVER_HOST0.0.0.0Interface to bind
SERVER_PORT8000Port to listen on

Configurable in the Settings UI

Open the gear icon in the web UI to change these at any time. A server restart is required for changes to take effect. See .env.example for the full list with comments.

VariableDefaultDescription
PROVIDERollamaLLM backend: ollama, openai, anthropic, fireworks
MODELqwen3Primary model for synthesis
FAST_MODELunsetSmaller model for Planner/Validator phases; falls back to MODEL
OLLAMA_BASE_URLhttp://localhost:11434Ollama server address
OPENAI_API_KEYRequired if PROVIDER=openai
ANTHROPIC_API_KEYRequired if PROVIDER=anthropic
FIREWORKS_API_KEYRequired if PROVIDER=fireworks
BRAVE_API_KEYBrave Search — 2K queries/month free
JINA_API_KEYJina Search + higher Reader rate limits
LOCATIONunsetCity/region injected into planner prompt for location-aware searches
CONTEXT_BUDGETcloudcloud / local / minimal — controls content truncation for local models
TEMPERATURE0.2LLM sampling temperature
TIMEOUT300LLM request timeout in seconds

Not in Settings UI (.env only)

VariableDefaultDescription
OLLAMA_API_KEYOllama Cloud search key — ollama.com
WEB_FETCH_PROVIDERhttphttp (raw fetch + Jina fallback) or jina (always use Jina Reader)
DISPATCH_DB_PATHsearch_history.dbSQLite path
DISPATCH_AGENTS_DIR./agentsYAML seed files directory
PLANNER_QUERY_COUNT5Global default for planner_query_count (overridden per-agent)
PARALLEL_SUB_PIPELINES1Max parallel workers for multi-track pipelines; 0 = sequential

Web UI

The web UI (/) is a single-page app. The main area shows the current conversation; the sidebar manages agents and schedules.

Tabs

Digest — a compiled briefing assembled from the most recent run of each active agent. Skips agents that haven't run yet.

Active — all enabled dispatches (agent + prompt + schedule). Each card shows the agent pill, label, next run time, and last run status. Click a card to view the last output, edit the schedule, or run it immediately.

Archived — disabled dispatches. Re-enable from here without losing the schedule.

Library — all agents in the database, whether scheduled or not. Browse, create, edit, and export agents here.

Agent type

When creating or editing an agent, select the agent type first:

  • Web Search — the planner generates queries and searches the web. Configure preferred domains, fetch content, and freshness filter.
  • Direct Fetch — provide a list of URLs to fetch directly (no search). {today} is substituted with the current date. Best for feeds and known API endpoints.

Tool bindings are inferred automatically from the type and don't need to be set manually.

Creating and scheduling an agent

  1. Library → New agent — give it a name, label, goal template, sources, and optionally a color swatch
  2. Save — the agent appears in the Library table immediately
  3. Click Schedule on the new row — the dispatch sheet opens with the agent pre-selected
  4. Enter the prompt the agent will run with (e.g. "SF local news digest"), set frequency and time, save
  5. The agent appears in Active and will run on schedule

Schedule Dispatch

Editing agents

Accessible from Library → Edit or by clicking a row. Fields map directly to the YAML schema above. Changes take effect on the next run without a server restart.

Settings

The gear icon (top-right) opens the Settings sheet. Changes are saved to .env and take effect after a server restart.

SectionWhat you can configure
LLM ProviderProvider, model, API key, Ollama base URL, fast model
SearchOllama Cloud, Brave, and Jina API keys
PersonalizationLocation (used by local-aware agents)
AdvancedContext budget, temperature, LLM timeout

Other UI features

  • + New — start a one-off prompt against any agent (no schedule)
  • think — toggle Qwen3 reasoning mode for the current prompt
  • Stop — cancel a running job; shows live elapsed time while running
  • Responses rendered as markdown with clickable links

CLI

python main.py --serve # start server
python main.py --agent news # interactive
python main.py --agent news --once # run once and exit
python main.py --agent apartment --once --query "2br sf soma under 3000"

Scheduling via system crontab (alternative to UI schedules)

0 7 ***cd /path/to/dispatch && python main.py --agent my-agent --once
0 8 ** 1 cd /path/to/dispatch && python main.py --agent my-agent --once --query "weekly recap"

Deploying as a service

A systemd service file is included at deploy/dispatch.service. Copy it to /etc/systemd/system/, update the paths, and:

sudo systemctl enable dispatch
sudo systemctl start dispatch

The server reads .env from the working directory and auto-restarts on failure.

Tools

ToolDescription
web_searchFan-out search across Ollama, Brave, Jina, and DDG in parallel; results merged and deduplicated
web_fetchFetch full page content from a URL; falls back to Jina Reader for JS-rendered or thin pages

Files

main.py # CLI entry point and dispatcher
pipeline.py # AgentPipeline and MultiAgentPipeline orchestrators
agent_loader.py # loads DB agent rows → DomainConfig objects at runtime
agent_config.py # DomainConfig and SubQueryConfig dataclasses
agents/ # optional YAML files — import via UI or POST /api/agents/import
server.py # FastAPI HTTP server and web UI
db.py # SQLite schema, CRUD, and YAML seed loader
tools.py # Tool definitions (web_search, web_fetch) and dispatch
ui.html # single-page web UI (served at /)
logs.py # CLI tool for reviewing session history
requirements.txt
.env.example
deploy/ # systemd service file
search_history.db # created on first run (gitignore this)

SQLite schema

sessions (id, model, agent, source, started_at, ended_at, viewed_at, total_duration_ms)
messages (id, session_id, role, content, created_at)
tool_calls (id, session_id, tool_name, args, result, duration_ms, created_at)
dispatches (id, agent, prompt, schedule, label, enabled, run_on_create, created_at,
trigger_type, repeat_until, max_runs, run_count)
agents (id, name, label, description, enabled, color,
goal_template, output_template,
sources, sub_queries, tool_bindings, direct_sources,
freshness, planner_query_count, max_validator_loops,
validate_links, fetch_content,
is_builtin, created_at, updated_at)
settings (key TEXTPRIMARY KEY, value TEXT, updated_at TEXT)

source in sessions is cli, http, or cron. role in messages is user, assistant, or error.

dispatches.schedule is a standard 5-field cron expression. Schedules are managed by APScheduler inside the server process and persist across restarts.

agents.color is a hue integer (0–359). Auto-assigned randomly when an agent is first created if not specified in YAML.

Querying history

CLI (logs.py)

python logs.py # last 50 sessions
python logs.py -n 100 # last 100 sessions
python logs.py --agent news # filter by agent
python logs.py --session 42 # full detail for session 42

Direct SQLite

-- Recent sessions with first promptSELECTs.id, s.agent, s.started_at,
(SELECT content FROM messages WHERE session_id=s.idAND role='user'ORDER BY id LIMIT1) AS prompt
FROM sessions s ORDER BYs.idDESCLIMIT20;
-- Search by keywordSELECT*FROM messages WHERE content LIKE'%keyword%';
-- Tool calls for a sessionSELECT tool_name, args, result FROM tool_calls WHERE session_id=42;

Python (db.py)

importdbsessions=db.get_sessions(limit=50, agent="news")
detail=db.get_session_detail(42)
results=db.search_sessions(agent="news", keyword="market")

HTTP API

GET / web UI
GET /health sanity check
POST /api/jobs submit a job (async)
GET /api/jobs/{job_id} poll status / result
DELETE /api/jobs/{job_id} cancel a running job
GET /api/sessions list sessions (?agent=news&limit=50)
GET /api/sessions/{id} full session detail (messages + tool calls with duration)
GET /api/dispatches list dispatches
POST /api/dispatches create a dispatch
PUT /api/dispatches/{id} update a dispatch
DELETE /api/dispatches/{id} delete a dispatch
GET /api/agents list all agents
GET /api/agents/{name} full agent detail
POST /api/agents create an agent
PUT /api/agents/{name} update an agent
DELETE /api/agents/{name} delete a non-builtin agent
GET /api/agents/{name}/export export agent as YAML
POST /api/agents/import create/update agent from YAML body
GET /api/tools list available tools
GET /api/settings current settings (values from live environment)
POST /api/settings save settings to DB + .env (restart required to apply)
POST /v1/chat/completions OpenAI-compatible (synchronous)
GET /v1/models

Authentication

python -c "import secrets; print(secrets.token_urlsafe(32))"

Set as SERVER_API_KEY=... in .env. If unset, all requests are accepted.

Note: When SERVER_API_KEY is set, the server injects it into the web UI's HTML on page load so the frontend can authenticate its API calls automatically. This means the key is visible in the browser's View Source on the / route. Keep the server on a trusted network (localhost, Tailscale, private LAN) — don't bind to a public interface without additional network-level access controls.

License

MIT — see LICENSE.

About

Self-hosted AI research agent platform. Define agents, schedule them as dispatches, and get synthesized digests — no manual searching. Bring your own LLM (Ollama, OpenAI, Anthropic, Fireworks).

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

dispatch

License: MIT

Self-hosted AI research agent platform. Define agents for topics you care about, schedule them as dispatches, and get synthesized digests — no manual searching. Runs on your machine or server. Bring your own LLM (Ollama, OpenAI, Anthropic, Fireworks).

Read Digest

What you get

  • Web UI to browse, schedule, and read agent output — no config files required
  • Daily digest compiled from the most recent run of each active agent
  • Multi-track pipelines that fan out searches across independent topic tracks in parallel

Quick Start

# 1. Create and activate a virtual environment
python -m venv dispatch-env
source dispatch-env/bin/activate
# 2. Install dependencies
pip install -r requirements.txt
# 3. Configure environment
cp .env.example .env
# Set SERVER_API_KEY if the server will be network-accessible (see .env.example)# Everything else — provider, model, API keys, location — can be set in the Settings UI# 4. Pull a model (Ollama only — skip if using a cloud provider)
ollama pull qwen3
# 5. Start the server
python main.py --serve # http://localhost:8000

Open the web UI, go to Library, create your first agent, then click Schedule to activate it.

How agents work

There are two distinct concepts: agents and dispatches.

  • Agent — what an agent is: its name, goal, sources, search settings, and color. Agents live in the database and are the source of truth for behavior.
  • Dispatch — an agent paired with a user prompt and a schedule. One agent can have multiple dispatches (e.g., the news agent running daily at 7am with a general prompt and again on Fridays with a weekly-recap prompt).

No agents are active by default. Create agents in the Library tab, then activate them by creating a dispatch.

Library tab — create and browse agents
↓
Schedule button → creates a dispatch (agent + prompt + schedule)
↓
Active tab — running scheduled dispatches

How it works

Each agent run uses a multi-phase micro-agent pipeline. Each phase is a small, focused LLM call with a constrained tool budget. The orchestrator (Python) manages state between phases; no single model call holds the full context.

query
→ Planner (LLM) — generate N search queries (or skip if direct_sources set)
→ Searcher (API) — fire all queries in parallel, collect results
→ Validator (LLM) — coverage sufficient? retry with new queries if not
→ Synthesizer (LLM) — format final answer from research buffer

Optional phases (enabled per-agent via config):

  • Link validator — HEAD-checks result URLs, drops dead ones before synthesis
  • Ranker + Fetcher — LLM selects top URLs → parallel web_fetch → per-page summarizer

Adding a new agent

Option 1 — Web UI

Configure Agent

Library tab → New agent → fill in name, goal, sources, and color → Save. The agent immediately appears in the Library and can be scheduled.

Option 2 — YAML

Drop .yaml files into your agents directory (defaults to ./agents, override with DISPATCH_AGENTS_DIR in .env). No agents folder is included in the repo — create it and add your own files.

Create agents/my-agent.yaml:

name: my-agentlabel: My Agentdescription: What this agent covers.color: 175# hue 0–359; omit to auto-assign on first seedgoal_template: "Find recent {query}. Focus on X, Y, Z."output_template: "Return a bulleted digest with source links."sources:
- example.com
- anothersource.orgtool_bindings:
- web_searchplanner_query_count: 5freshness: w # d=24h w=7d m=30d y=1y (omit = any date)

Restart the server — the agent appears in the Library tab. Click Schedule to activate it.

Option 3 — Import YAML

Library tab → Import or POST /api/agents/import with a YAML body. Useful for migrating agents between instances.

YAML field reference

FieldTypeDefaultDescription
namestringrequiredUnique slug — used as the agent identifier everywhere
labelstring""Display name shown in pills and headers
descriptionstring""Short summary shown in the Library table
colorintegerrandomHue (0–359) for the color pill. Auto-assigned on first seed if omitted; never overwritten by re-seeding
goal_templatestringrequiredLLM prompt for what to find. {query} → user input, {today} → YYYY-MM-DD
output_templatestring""Instructions for how the synthesizer should format the response
sourceslist[]Domains to bias planner queries toward (e.g. sfchronicle.com)
direct_sourceslist[]URLs fetched directly, bypassing the planner. {today} substitution supported
sub_querieslistnullMulti-track pipelines — each track runs an isolated search pass (see Pipeline modes)
tool_bindingslist["web_search"]Tools available to this agent. Empty list = all tools
freshnessstringnullDate filter: dwmy
planner_query_countinteger5Number of search queries the planner generates
max_validator_loopsinteger1How many times the validator can request additional searches
validate_linksboolfalseHEAD-check each result URL and drop dead ones before synthesis
fetch_contentboolfalseRank URLs by relevance, fetch top 5 full pages, summarize per page

Pipeline modes

The pipeline is selected automatically based on your config. All modes end with a Validator loop and a Synthesizer call.

1. Search (default)

# no direct_sourcestool_bindings: [web_search]

LLM planner generates planner_query_count queries → parallel web search across all providers → validate → synthesize.

Best for news, research, any topic where broad query coverage helps. sources narrows the planner's focus toward specific domains without locking it to them.

2. Search + deep read

tool_bindings: [web_search, web_fetch]fetch_content: true

Same as search, but after collecting results the pipeline ranks all URLs by relevance, fetches the top 5 full pages, runs a per-page summarizer, and uses the extracted facts for synthesis instead of raw snippets.

Best for agents where the article body matters — detailed reporting, analysis, documentation. Slower (~2× more LLM calls) but higher-quality output.

3. Direct feed

direct_sources:
- https://example.com/feed?date={today}
- https://api.example.com/poststool_bindings: [web_fetch] # required — or omit tool_bindings entirely

Skips the LLM planner entirely. Each URL is fetched directly (with {today} substituted as YYYY-MM-DD). Results go straight into the research buffer.

Best for RSS feeds, known API endpoints, or structured data pages updated on a schedule. Note: tool_bindings must include web_fetch or be empty — the default ["web_search"] will silently skip direct URLs.

4. Domain-biased search

sources:
- sfchronicle.com
- missionlocal.org

The LLM planner is given sources as a hint — it generates queries biased toward those domains but still searches the open web. Not a hard filter.

Best for agents that should prefer specific publications without being locked to them.

5. Multi-track

sub_queries:
- label: researchgoal: "Find recent ML papers about {query}"sources: [arxiv.org, paperswithcode.com]query_count: 3freshness: w
- label: industrygoal: "Find business news about {query} in AI"sources: [techcrunch.com, venturebeat.com]query_count: 3

N independent search pipelines run in parallel, each with its own goal, sources, freshness, and query count. Results are aggregated into a final synthesizer call.

Best for agents covering multiple distinct topics that benefit from isolated query sets (e.g., AI papers vs industry news vs tooling). Note: fetch_content and direct_sources are not inherited by sub-pipelines — each track is search-only.

Options that apply across all modes

validate_links — HEAD-checks every result URL and drops dead ones before synthesis. Adds a few seconds, improves source quality.

max_validator_loops — the validator judges whether the research buffer is sufficient. If not, it generates retry queries and reruns the search phase. Default 1 means one pass (no retry). Set to 23 for agents where completeness matters more than speed.

LLM providers

Configure via the Settings UI (gear icon) or set in .env. Cloud providers automatically use larger context windows and higher parallelism than local Ollama.

ProviderPROVIDER=Recommended models
Ollama (default)ollamaqwen3 / qwen3:1.7b (fast)
OpenAIopenaigpt-4.1 / gpt-4.1-mini (fast)
Anthropicanthropicclaude-sonnet-4-6 / claude-haiku-4-5-20251001 (fast)
Fireworksfireworksllama-v3p3-70b-instruct / llama-v3p1-8b-instruct (fast)

Set MODEL for the primary model and optionally FAST_MODEL for a smaller model used in the Planner/Validator phases.

Search providers

Web search fans out across multiple providers in parallel, merges and deduplicates results. DuckDuckGo works out of the box — no key required. Add any of the following free-tier providers to expand coverage:

ProviderKeyFree tierNotes
Ollama CloudOLLAMA_API_KEYollama.comRecommended; primary fan-out provider
Brave SearchBRAVE_API_KEY2K queries/month — brave.com/search/apiGood general coverage
Jina SearchJINA_API_KEYjina.aiAlso enables Jina Reader for page fetching; site: queries excluded
DuckDuckGononealways-onBaseline; 60s circuit-breaker, auto-recovers

Page fetching uses requests with automatic fallback to Jina Reader (r.jina.ai) when a page returns thin content — catches JavaScript-rendered shells transparently. Set WEB_FETCH_PROVIDER=jina to always use Jina.

Environment variables

Required (set in .env before first run)

These are read at startup before the UI is accessible and cannot be changed via the Settings UI.

VariableDefaultDescription
SERVER_API_KEYunsetBearer token for the HTTP API. Set this if the server is network-accessible — without it anyone who can reach the host can trigger runs and read your data. Not needed for localhost use.
SERVER_HOST0.0.0.0Interface to bind
SERVER_PORT8000Port to listen on

Configurable in the Settings UI

Open the gear icon in the web UI to change these at any time. A server restart is required for changes to take effect. See .env.example for the full list with comments.

VariableDefaultDescription
PROVIDERollamaLLM backend: ollama, openai, anthropic, fireworks
MODELqwen3Primary model for synthesis
FAST_MODELunsetSmaller model for Planner/Validator phases; falls back to MODEL
OLLAMA_BASE_URLhttp://localhost:11434Ollama server address
OPENAI_API_KEYRequired if PROVIDER=openai
ANTHROPIC_API_KEYRequired if PROVIDER=anthropic
FIREWORKS_API_KEYRequired if PROVIDER=fireworks
BRAVE_API_KEYBrave Search — 2K queries/month free
JINA_API_KEYJina Search + higher Reader rate limits
LOCATIONunsetCity/region injected into planner prompt for location-aware searches
CONTEXT_BUDGETcloudcloud / local / minimal — controls content truncation for local models
TEMPERATURE0.2LLM sampling temperature
TIMEOUT300LLM request timeout in seconds

Not in Settings UI (.env only)

VariableDefaultDescription
OLLAMA_API_KEYOllama Cloud search key — ollama.com
WEB_FETCH_PROVIDERhttphttp (raw fetch + Jina fallback) or jina (always use Jina Reader)
DISPATCH_DB_PATHsearch_history.dbSQLite path
DISPATCH_AGENTS_DIR./agentsYAML seed files directory
PLANNER_QUERY_COUNT5Global default for planner_query_count (overridden per-agent)
PARALLEL_SUB_PIPELINES1Max parallel workers for multi-track pipelines; 0 = sequential

Web UI

The web UI (/) is a single-page app. The main area shows the current conversation; the sidebar manages agents and schedules.

Tabs

Digest — a compiled briefing assembled from the most recent run of each active agent. Skips agents that haven't run yet.

Active — all enabled dispatches (agent + prompt + schedule). Each card shows the agent pill, label, next run time, and last run status. Click a card to view the last output, edit the schedule, or run it immediately.

Archived — disabled dispatches. Re-enable from here without losing the schedule.

Library — all agents in the database, whether scheduled or not. Browse, create, edit, and export agents here.

Agent type

When creating or editing an agent, select the agent type first:

  • Web Search — the planner generates queries and searches the web. Configure preferred domains, fetch content, and freshness filter.
  • Direct Fetch — provide a list of URLs to fetch directly (no search). {today} is substituted with the current date. Best for feeds and known API endpoints.

Tool bindings are inferred automatically from the type and don't need to be set manually.

Creating and scheduling an agent

  1. Library → New agent — give it a name, label, goal template, sources, and optionally a color swatch
  2. Save — the agent appears in the Library table immediately
  3. Click Schedule on the new row — the dispatch sheet opens with the agent pre-selected
  4. Enter the prompt the agent will run with (e.g. "SF local news digest"), set frequency and time, save
  5. The agent appears in Active and will run on schedule

Schedule Dispatch

Editing agents

Accessible from Library → Edit or by clicking a row. Fields map directly to the YAML schema above. Changes take effect on the next run without a server restart.

Settings

The gear icon (top-right) opens the Settings sheet. Changes are saved to .env and take effect after a server restart.

SectionWhat you can configure
LLM ProviderProvider, model, API key, Ollama base URL, fast model
SearchOllama Cloud, Brave, and Jina API keys
PersonalizationLocation (used by local-aware agents)
AdvancedContext budget, temperature, LLM timeout

Other UI features

  • + New — start a one-off prompt against any agent (no schedule)
  • think — toggle Qwen3 reasoning mode for the current prompt
  • Stop — cancel a running job; shows live elapsed time while running
  • Responses rendered as markdown with clickable links

CLI

python main.py --serve # start server
python main.py --agent news # interactive
python main.py --agent news --once # run once and exit
python main.py --agent apartment --once --query "2br sf soma under 3000"

Scheduling via system crontab (alternative to UI schedules)

0 7 ***cd /path/to/dispatch && python main.py --agent my-agent --once
0 8 ** 1 cd /path/to/dispatch && python main.py --agent my-agent --once --query "weekly recap"

Deploying as a service

A systemd service file is included at deploy/dispatch.service. Copy it to /etc/systemd/system/, update the paths, and:

sudo systemctl enable dispatch
sudo systemctl start dispatch

The server reads .env from the working directory and auto-restarts on failure.

Tools

ToolDescription
web_searchFan-out search across Ollama, Brave, Jina, and DDG in parallel; results merged and deduplicated
web_fetchFetch full page content from a URL; falls back to Jina Reader for JS-rendered or thin pages

Files

main.py # CLI entry point and dispatcher
pipeline.py # AgentPipeline and MultiAgentPipeline orchestrators
agent_loader.py # loads DB agent rows → DomainConfig objects at runtime
agent_config.py # DomainConfig and SubQueryConfig dataclasses
agents/ # optional YAML files — import via UI or POST /api/agents/import
server.py # FastAPI HTTP server and web UI
db.py # SQLite schema, CRUD, and YAML seed loader
tools.py # Tool definitions (web_search, web_fetch) and dispatch
ui.html # single-page web UI (served at /)
logs.py # CLI tool for reviewing session history
requirements.txt
.env.example
deploy/ # systemd service file
search_history.db # created on first run (gitignore this)

SQLite schema

sessions (id, model, agent, source, started_at, ended_at, viewed_at, total_duration_ms)
messages (id, session_id, role, content, created_at)
tool_calls (id, session_id, tool_name, args, result, duration_ms, created_at)
dispatches (id, agent, prompt, schedule, label, enabled, run_on_create, created_at,
trigger_type, repeat_until, max_runs, run_count)
agents (id, name, label, description, enabled, color,
goal_template, output_template,
sources, sub_queries, tool_bindings, direct_sources,
freshness, planner_query_count, max_validator_loops,
validate_links, fetch_content,
is_builtin, created_at, updated_at)
settings (key TEXTPRIMARY KEY, value TEXT, updated_at TEXT)

source in sessions is cli, http, or cron. role in messages is user, assistant, or error.

dispatches.schedule is a standard 5-field cron expression. Schedules are managed by APScheduler inside the server process and persist across restarts.

agents.color is a hue integer (0–359). Auto-assigned randomly when an agent is first created if not specified in YAML.

Querying history

CLI (logs.py)

python logs.py # last 50 sessions
python logs.py -n 100 # last 100 sessions
python logs.py --agent news # filter by agent
python logs.py --session 42 # full detail for session 42

Direct SQLite

-- Recent sessions with first promptSELECTs.id, s.agent, s.started_at,
(SELECT content FROM messages WHERE session_id=s.idAND role='user'ORDER BY id LIMIT1) AS prompt
FROM sessions s ORDER BYs.idDESCLIMIT20;
-- Search by keywordSELECT*FROM messages WHERE content LIKE'%keyword%';
-- Tool calls for a sessionSELECT tool_name, args, result FROM tool_calls WHERE session_id=42;

Python (db.py)

importdbsessions=db.get_sessions(limit=50, agent="news")
detail=db.get_session_detail(42)
results=db.search_sessions(agent="news", keyword="market")

HTTP API

GET / web UI
GET /health sanity check
POST /api/jobs submit a job (async)
GET /api/jobs/{job_id} poll status / result
DELETE /api/jobs/{job_id} cancel a running job
GET /api/sessions list sessions (?agent=news&limit=50)
GET /api/sessions/{id} full session detail (messages + tool calls with duration)
GET /api/dispatches list dispatches
POST /api/dispatches create a dispatch
PUT /api/dispatches/{id} update a dispatch
DELETE /api/dispatches/{id} delete a dispatch
GET /api/agents list all agents
GET /api/agents/{name} full agent detail
POST /api/agents create an agent
PUT /api/agents/{name} update an agent
DELETE /api/agents/{name} delete a non-builtin agent
GET /api/agents/{name}/export export agent as YAML
POST /api/agents/import create/update agent from YAML body
GET /api/tools list available tools
GET /api/settings current settings (values from live environment)
POST /api/settings save settings to DB + .env (restart required to apply)
POST /v1/chat/completions OpenAI-compatible (synchronous)
GET /v1/models

Authentication

python -c "import secrets; print(secrets.token_urlsafe(32))"

Set as SERVER_API_KEY=... in .env. If unset, all requests are accepted.

Note: When SERVER_API_KEY is set, the server injects it into the web UI's HTML on page load so the frontend can authenticate its API calls automatically. This means the key is visible in the browser's View Source on the / route. Keep the server on a trusted network (localhost, Tailscale, private LAN) — don't bind to a public interface without additional network-level access controls.

License

MIT — see LICENSE.

About

Self-hosted AI research agent platform. Define agents, schedule them as dispatches, and get synthesized digests — no manual searching. Bring your own LLM (Ollama, OpenAI, Anthropic, Fireworks).

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

dispatch

License: MIT

Self-hosted AI research agent platform. Define agents for topics you care about, schedule them as dispatches, and get synthesized digests — no manual searching. Runs on your machine or server. Bring your own LLM (Ollama, OpenAI, Anthropic, Fireworks).

Read Digest

What you get

  • Web UI to browse, schedule, and read agent output — no config files required
  • Daily digest compiled from the most recent run of each active agent
  • Multi-track pipelines that fan out searches across independent topic tracks in parallel

Quick Start

# 1. Create and activate a virtual environment
python -m venv dispatch-env
source dispatch-env/bin/activate
# 2. Install dependencies
pip install -r requirements.txt
# 3. Configure environment
cp .env.example .env
# Set SERVER_API_KEY if the server will be network-accessible (see .env.example)# Everything else — provider, model, API keys, location — can be set in the Settings UI# 4. Pull a model (Ollama only — skip if using a cloud provider)
ollama pull qwen3
# 5. Start the server
python main.py --serve # http://localhost:8000

Open the web UI, go to Library, create your first agent, then click Schedule to activate it.

How agents work

There are two distinct concepts: agents and dispatches.

  • Agent — what an agent is: its name, goal, sources, search settings, and color. Agents live in the database and are the source of truth for behavior.
  • Dispatch — an agent paired with a user prompt and a schedule. One agent can have multiple dispatches (e.g., the news agent running daily at 7am with a general prompt and again on Fridays with a weekly-recap prompt).

No agents are active by default. Create agents in the Library tab, then activate them by creating a dispatch.

Library tab — create and browse agents
↓
Schedule button → creates a dispatch (agent + prompt + schedule)
↓
Active tab — running scheduled dispatches

How it works

Each agent run uses a multi-phase micro-agent pipeline. Each phase is a small, focused LLM call with a constrained tool budget. The orchestrator (Python) manages state between phases; no single model call holds the full context.

query
→ Planner (LLM) — generate N search queries (or skip if direct_sources set)
→ Searcher (API) — fire all queries in parallel, collect results
→ Validator (LLM) — coverage sufficient? retry with new queries if not
→ Synthesizer (LLM) — format final answer from research buffer

Optional phases (enabled per-agent via config):

  • Link validator — HEAD-checks result URLs, drops dead ones before synthesis
  • Ranker + Fetcher — LLM selects top URLs → parallel web_fetch → per-page summarizer

Adding a new agent

Option 1 — Web UI

Configure Agent

Library tab → New agent → fill in name, goal, sources, and color → Save. The agent immediately appears in the Library and can be scheduled.

Option 2 — YAML

Drop .yaml files into your agents directory (defaults to ./agents, override with DISPATCH_AGENTS_DIR in .env). No agents folder is included in the repo — create it and add your own files.

Create agents/my-agent.yaml:

name: my-agentlabel: My Agentdescription: What this agent covers.color: 175# hue 0–359; omit to auto-assign on first seedgoal_template: "Find recent {query}. Focus on X, Y, Z."output_template: "Return a bulleted digest with source links."sources:
- example.com
- anothersource.orgtool_bindings:
- web_searchplanner_query_count: 5freshness: w # d=24h w=7d m=30d y=1y (omit = any date)

Restart the server — the agent appears in the Library tab. Click Schedule to activate it.

Option 3 — Import YAML

Library tab → Import or POST /api/agents/import with a YAML body. Useful for migrating agents between instances.

YAML field reference

FieldTypeDefaultDescription
namestringrequiredUnique slug — used as the agent identifier everywhere
labelstring""Display name shown in pills and headers
descriptionstring""Short summary shown in the Library table
colorintegerrandomHue (0–359) for the color pill. Auto-assigned on first seed if omitted; never overwritten by re-seeding
goal_templatestringrequiredLLM prompt for what to find. {query} → user input, {today} → YYYY-MM-DD
output_templatestring""Instructions for how the synthesizer should format the response
sourceslist[]Domains to bias planner queries toward (e.g. sfchronicle.com)
direct_sourceslist[]URLs fetched directly, bypassing the planner. {today} substitution supported
sub_querieslistnullMulti-track pipelines — each track runs an isolated search pass (see Pipeline modes)
tool_bindingslist["web_search"]Tools available to this agent. Empty list = all tools
freshnessstringnullDate filter: dwmy
planner_query_countinteger5Number of search queries the planner generates
max_validator_loopsinteger1How many times the validator can request additional searches
validate_linksboolfalseHEAD-check each result URL and drop dead ones before synthesis
fetch_contentboolfalseRank URLs by relevance, fetch top 5 full pages, summarize per page

Pipeline modes

The pipeline is selected automatically based on your config. All modes end with a Validator loop and a Synthesizer call.

1. Search (default)

# no direct_sourcestool_bindings: [web_search]

LLM planner generates planner_query_count queries → parallel web search across all providers → validate → synthesize.

Best for news, research, any topic where broad query coverage helps. sources narrows the planner's focus toward specific domains without locking it to them.

2. Search + deep read

tool_bindings: [web_search, web_fetch]fetch_content: true

Same as search, but after collecting results the pipeline ranks all URLs by relevance, fetches the top 5 full pages, runs a per-page summarizer, and uses the extracted facts for synthesis instead of raw snippets.

Best for agents where the article body matters — detailed reporting, analysis, documentation. Slower (~2× more LLM calls) but higher-quality output.

3. Direct feed

direct_sources:
- https://example.com/feed?date={today}
- https://api.example.com/poststool_bindings: [web_fetch] # required — or omit tool_bindings entirely

Skips the LLM planner entirely. Each URL is fetched directly (with {today} substituted as YYYY-MM-DD). Results go straight into the research buffer.

Best for RSS feeds, known API endpoints, or structured data pages updated on a schedule. Note: tool_bindings must include web_fetch or be empty — the default ["web_search"] will silently skip direct URLs.

4. Domain-biased search

sources:
- sfchronicle.com
- missionlocal.org

The LLM planner is given sources as a hint — it generates queries biased toward those domains but still searches the open web. Not a hard filter.

Best for agents that should prefer specific publications without being locked to them.

5. Multi-track

sub_queries:
- label: researchgoal: "Find recent ML papers about {query}"sources: [arxiv.org, paperswithcode.com]query_count: 3freshness: w
- label: industrygoal: "Find business news about {query} in AI"sources: [techcrunch.com, venturebeat.com]query_count: 3

N independent search pipelines run in parallel, each with its own goal, sources, freshness, and query count. Results are aggregated into a final synthesizer call.

Best for agents covering multiple distinct topics that benefit from isolated query sets (e.g., AI papers vs industry news vs tooling). Note: fetch_content and direct_sources are not inherited by sub-pipelines — each track is search-only.

Options that apply across all modes

validate_links — HEAD-checks every result URL and drops dead ones before synthesis. Adds a few seconds, improves source quality.

max_validator_loops — the validator judges whether the research buffer is sufficient. If not, it generates retry queries and reruns the search phase. Default 1 means one pass (no retry). Set to 23 for agents where completeness matters more than speed.

LLM providers

Configure via the Settings UI (gear icon) or set in .env. Cloud providers automatically use larger context windows and higher parallelism than local Ollama.

ProviderPROVIDER=Recommended models
Ollama (default)ollamaqwen3 / qwen3:1.7b (fast)
OpenAIopenaigpt-4.1 / gpt-4.1-mini (fast)
Anthropicanthropicclaude-sonnet-4-6 / claude-haiku-4-5-20251001 (fast)
Fireworksfireworksllama-v3p3-70b-instruct / llama-v3p1-8b-instruct (fast)

Set MODEL for the primary model and optionally FAST_MODEL for a smaller model used in the Planner/Validator phases.

Search providers

Web search fans out across multiple providers in parallel, merges and deduplicates results. DuckDuckGo works out of the box — no key required. Add any of the following free-tier providers to expand coverage:

ProviderKeyFree tierNotes
Ollama CloudOLLAMA_API_KEYollama.comRecommended; primary fan-out provider
Brave SearchBRAVE_API_KEY2K queries/month — brave.com/search/apiGood general coverage
Jina SearchJINA_API_KEYjina.aiAlso enables Jina Reader for page fetching; site: queries excluded
DuckDuckGononealways-onBaseline; 60s circuit-breaker, auto-recovers

Page fetching uses requests with automatic fallback to Jina Reader (r.jina.ai) when a page returns thin content — catches JavaScript-rendered shells transparently. Set WEB_FETCH_PROVIDER=jina to always use Jina.

Environment variables

Required (set in .env before first run)

These are read at startup before the UI is accessible and cannot be changed via the Settings UI.

VariableDefaultDescription
SERVER_API_KEYunsetBearer token for the HTTP API. Set this if the server is network-accessible — without it anyone who can reach the host can trigger runs and read your data. Not needed for localhost use.
SERVER_HOST0.0.0.0Interface to bind
SERVER_PORT8000Port to listen on

Configurable in the Settings UI

Open the gear icon in the web UI to change these at any time. A server restart is required for changes to take effect. See .env.example for the full list with comments.

VariableDefaultDescription
PROVIDERollamaLLM backend: ollama, openai, anthropic, fireworks
MODELqwen3Primary model for synthesis
FAST_MODELunsetSmaller model for Planner/Validator phases; falls back to MODEL
OLLAMA_BASE_URLhttp://localhost:11434Ollama server address
OPENAI_API_KEYRequired if PROVIDER=openai
ANTHROPIC_API_KEYRequired if PROVIDER=anthropic
FIREWORKS_API_KEYRequired if PROVIDER=fireworks
BRAVE_API_KEYBrave Search — 2K queries/month free
JINA_API_KEYJina Search + higher Reader rate limits
LOCATIONunsetCity/region injected into planner prompt for location-aware searches
CONTEXT_BUDGETcloudcloud / local / minimal — controls content truncation for local models
TEMPERATURE0.2LLM sampling temperature
TIMEOUT300LLM request timeout in seconds

Not in Settings UI (.env only)

VariableDefaultDescription
OLLAMA_API_KEYOllama Cloud search key — ollama.com
WEB_FETCH_PROVIDERhttphttp (raw fetch + Jina fallback) or jina (always use Jina Reader)
DISPATCH_DB_PATHsearch_history.dbSQLite path
DISPATCH_AGENTS_DIR./agentsYAML seed files directory
PLANNER_QUERY_COUNT5Global default for planner_query_count (overridden per-agent)
PARALLEL_SUB_PIPELINES1Max parallel workers for multi-track pipelines; 0 = sequential

Web UI

The web UI (/) is a single-page app. The main area shows the current conversation; the sidebar manages agents and schedules.

Tabs

Digest — a compiled briefing assembled from the most recent run of each active agent. Skips agents that haven't run yet.

Active — all enabled dispatches (agent + prompt + schedule). Each card shows the agent pill, label, next run time, and last run status. Click a card to view the last output, edit the schedule, or run it immediately.

Archived — disabled dispatches. Re-enable from here without losing the schedule.

Library — all agents in the database, whether scheduled or not. Browse, create, edit, and export agents here.

Agent type

When creating or editing an agent, select the agent type first:

  • Web Search — the planner generates queries and searches the web. Configure preferred domains, fetch content, and freshness filter.
  • Direct Fetch — provide a list of URLs to fetch directly (no search). {today} is substituted with the current date. Best for feeds and known API endpoints.

Tool bindings are inferred automatically from the type and don't need to be set manually.

Creating and scheduling an agent

  1. Library → New agent — give it a name, label, goal template, sources, and optionally a color swatch
  2. Save — the agent appears in the Library table immediately
  3. Click Schedule on the new row — the dispatch sheet opens with the agent pre-selected
  4. Enter the prompt the agent will run with (e.g. "SF local news digest"), set frequency and time, save
  5. The agent appears in Active and will run on schedule

Schedule Dispatch

Editing agents

Accessible from Library → Edit or by clicking a row. Fields map directly to the YAML schema above. Changes take effect on the next run without a server restart.

Settings

The gear icon (top-right) opens the Settings sheet. Changes are saved to .env and take effect after a server restart.

SectionWhat you can configure
LLM ProviderProvider, model, API key, Ollama base URL, fast model
SearchOllama Cloud, Brave, and Jina API keys
PersonalizationLocation (used by local-aware agents)
AdvancedContext budget, temperature, LLM timeout

Other UI features

  • + New — start a one-off prompt against any agent (no schedule)
  • think — toggle Qwen3 reasoning mode for the current prompt
  • Stop — cancel a running job; shows live elapsed time while running
  • Responses rendered as markdown with clickable links

CLI

python main.py --serve # start server
python main.py --agent news # interactive
python main.py --agent news --once # run once and exit
python main.py --agent apartment --once --query "2br sf soma under 3000"

Scheduling via system crontab (alternative to UI schedules)

0 7 ***cd /path/to/dispatch && python main.py --agent my-agent --once
0 8 ** 1 cd /path/to/dispatch && python main.py --agent my-agent --once --query "weekly recap"

Deploying as a service

A systemd service file is included at deploy/dispatch.service. Copy it to /etc/systemd/system/, update the paths, and:

sudo systemctl enable dispatch
sudo systemctl start dispatch

The server reads .env from the working directory and auto-restarts on failure.

Tools

ToolDescription
web_searchFan-out search across Ollama, Brave, Jina, and DDG in parallel; results merged and deduplicated
web_fetchFetch full page content from a URL; falls back to Jina Reader for JS-rendered or thin pages

Files

main.py # CLI entry point and dispatcher
pipeline.py # AgentPipeline and MultiAgentPipeline orchestrators
agent_loader.py # loads DB agent rows → DomainConfig objects at runtime
agent_config.py # DomainConfig and SubQueryConfig dataclasses
agents/ # optional YAML files — import via UI or POST /api/agents/import
server.py # FastAPI HTTP server and web UI
db.py # SQLite schema, CRUD, and YAML seed loader
tools.py # Tool definitions (web_search, web_fetch) and dispatch
ui.html # single-page web UI (served at /)
logs.py # CLI tool for reviewing session history
requirements.txt
.env.example
deploy/ # systemd service file
search_history.db # created on first run (gitignore this)

SQLite schema

sessions (id, model, agent, source, started_at, ended_at, viewed_at, total_duration_ms)
messages (id, session_id, role, content, created_at)
tool_calls (id, session_id, tool_name, args, result, duration_ms, created_at)
dispatches (id, agent, prompt, schedule, label, enabled, run_on_create, created_at,
trigger_type, repeat_until, max_runs, run_count)
agents (id, name, label, description, enabled, color,
goal_template, output_template,
sources, sub_queries, tool_bindings, direct_sources,
freshness, planner_query_count, max_validator_loops,
validate_links, fetch_content,
is_builtin, created_at, updated_at)
settings (key TEXTPRIMARY KEY, value TEXT, updated_at TEXT)

source in sessions is cli, http, or cron. role in messages is user, assistant, or error.

dispatches.schedule is a standard 5-field cron expression. Schedules are managed by APScheduler inside the server process and persist across restarts.

agents.color is a hue integer (0–359). Auto-assigned randomly when an agent is first created if not specified in YAML.

Querying history

CLI (logs.py)

python logs.py # last 50 sessions
python logs.py -n 100 # last 100 sessions
python logs.py --agent news # filter by agent
python logs.py --session 42 # full detail for session 42

Direct SQLite

-- Recent sessions with first promptSELECTs.id, s.agent, s.started_at,
(SELECT content FROM messages WHERE session_id=s.idAND role='user'ORDER BY id LIMIT1) AS prompt
FROM sessions s ORDER BYs.idDESCLIMIT20;
-- Search by keywordSELECT*FROM messages WHERE content LIKE'%keyword%';
-- Tool calls for a sessionSELECT tool_name, args, result FROM tool_calls WHERE session_id=42;

Python (db.py)

importdbsessions=db.get_sessions(limit=50, agent="news")
detail=db.get_session_detail(42)
results=db.search_sessions(agent="news", keyword="market")

HTTP API

GET / web UI
GET /health sanity check
POST /api/jobs submit a job (async)
GET /api/jobs/{job_id} poll status / result
DELETE /api/jobs/{job_id} cancel a running job
GET /api/sessions list sessions (?agent=news&limit=50)
GET /api/sessions/{id} full session detail (messages + tool calls with duration)
GET /api/dispatches list dispatches
POST /api/dispatches create a dispatch
PUT /api/dispatches/{id} update a dispatch
DELETE /api/dispatches/{id} delete a dispatch
GET /api/agents list all agents
GET /api/agents/{name} full agent detail
POST /api/agents create an agent
PUT /api/agents/{name} update an agent
DELETE /api/agents/{name} delete a non-builtin agent
GET /api/agents/{name}/export export agent as YAML
POST /api/agents/import create/update agent from YAML body
GET /api/tools list available tools
GET /api/settings current settings (values from live environment)
POST /api/settings save settings to DB + .env (restart required to apply)
POST /v1/chat/completions OpenAI-compatible (synchronous)
GET /v1/models

Authentication

python -c "import secrets; print(secrets.token_urlsafe(32))"

Set as SERVER_API_KEY=... in .env. If unset, all requests are accepted.

Note: When SERVER_API_KEY is set, the server injects it into the web UI's HTML on page load so the frontend can authenticate its API calls automatically. This means the key is visible in the browser's View Source on the / route. Keep the server on a trusted network (localhost, Tailscale, private LAN) — don't bind to a public interface without additional network-level access controls.

License

MIT — see LICENSE.

About

Self-hosted AI research agent platform. Define agents, schedule them as dispatches, and get synthesized digests — no manual searching. Bring your own LLM (Ollama, OpenAI, Anthropic, Fireworks).

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

dispatch

License: MIT

Self-hosted AI research agent platform. Define agents for topics you care about, schedule them as dispatches, and get synthesized digests — no manual searching. Runs on your machine or server. Bring your own LLM (Ollama, OpenAI, Anthropic, Fireworks).

Read Digest

What you get

  • Web UI to browse, schedule, and read agent output — no config files required
  • Daily digest compiled from the most recent run of each active agent
  • Multi-track pipelines that fan out searches across independent topic tracks in parallel

Quick Start

# 1. Create and activate a virtual environment
python -m venv dispatch-env
source dispatch-env/bin/activate
# 2. Install dependencies
pip install -r requirements.txt
# 3. Configure environment
cp .env.example .env
# Set SERVER_API_KEY if the server will be network-accessible (see .env.example)# Everything else — provider, model, API keys, location — can be set in the Settings UI# 4. Pull a model (Ollama only — skip if using a cloud provider)
ollama pull qwen3
# 5. Start the server
python main.py --serve # http://localhost:8000

Open the web UI, go to Library, create your first agent, then click Schedule to activate it.

How agents work

There are two distinct concepts: agents and dispatches.

  • Agent — what an agent is: its name, goal, sources, search settings, and color. Agents live in the database and are the source of truth for behavior.
  • Dispatch — an agent paired with a user prompt and a schedule. One agent can have multiple dispatches (e.g., the news agent running daily at 7am with a general prompt and again on Fridays with a weekly-recap prompt).

No agents are active by default. Create agents in the Library tab, then activate them by creating a dispatch.

Library tab — create and browse agents
↓
Schedule button → creates a dispatch (agent + prompt + schedule)
↓
Active tab — running scheduled dispatches

How it works

Each agent run uses a multi-phase micro-agent pipeline. Each phase is a small, focused LLM call with a constrained tool budget. The orchestrator (Python) manages state between phases; no single model call holds the full context.

query
→ Planner (LLM) — generate N search queries (or skip if direct_sources set)
→ Searcher (API) — fire all queries in parallel, collect results
→ Validator (LLM) — coverage sufficient? retry with new queries if not
→ Synthesizer (LLM) — format final answer from research buffer

Optional phases (enabled per-agent via config):

  • Link validator — HEAD-checks result URLs, drops dead ones before synthesis
  • Ranker + Fetcher — LLM selects top URLs → parallel web_fetch → per-page summarizer

Adding a new agent

Option 1 — Web UI

Configure Agent

Library tab → New agent → fill in name, goal, sources, and color → Save. The agent immediately appears in the Library and can be scheduled.

Option 2 — YAML

Drop .yaml files into your agents directory (defaults to ./agents, override with DISPATCH_AGENTS_DIR in .env). No agents folder is included in the repo — create it and add your own files.

Create agents/my-agent.yaml:

name: my-agentlabel: My Agentdescription: What this agent covers.color: 175# hue 0–359; omit to auto-assign on first seedgoal_template: "Find recent {query}. Focus on X, Y, Z."output_template: "Return a bulleted digest with source links."sources:
- example.com
- anothersource.orgtool_bindings:
- web_searchplanner_query_count: 5freshness: w # d=24h w=7d m=30d y=1y (omit = any date)

Restart the server — the agent appears in the Library tab. Click Schedule to activate it.

Option 3 — Import YAML

Library tab → Import or POST /api/agents/import with a YAML body. Useful for migrating agents between instances.

YAML field reference

FieldTypeDefaultDescription
namestringrequiredUnique slug — used as the agent identifier everywhere
labelstring""Display name shown in pills and headers
descriptionstring""Short summary shown in the Library table
colorintegerrandomHue (0–359) for the color pill. Auto-assigned on first seed if omitted; never overwritten by re-seeding
goal_templatestringrequiredLLM prompt for what to find. {query} → user input, {today} → YYYY-MM-DD
output_templatestring""Instructions for how the synthesizer should format the response
sourceslist[]Domains to bias planner queries toward (e.g. sfchronicle.com)
direct_sourceslist[]URLs fetched directly, bypassing the planner. {today} substitution supported
sub_querieslistnullMulti-track pipelines — each track runs an isolated search pass (see Pipeline modes)
tool_bindingslist["web_search"]Tools available to this agent. Empty list = all tools
freshnessstringnullDate filter: dwmy
planner_query_countinteger5Number of search queries the planner generates
max_validator_loopsinteger1How many times the validator can request additional searches
validate_linksboolfalseHEAD-check each result URL and drop dead ones before synthesis
fetch_contentboolfalseRank URLs by relevance, fetch top 5 full pages, summarize per page

Pipeline modes

The pipeline is selected automatically based on your config. All modes end with a Validator loop and a Synthesizer call.

1. Search (default)

# no direct_sourcestool_bindings: [web_search]

LLM planner generates planner_query_count queries → parallel web search across all providers → validate → synthesize.

Best for news, research, any topic where broad query coverage helps. sources narrows the planner's focus toward specific domains without locking it to them.

2. Search + deep read

tool_bindings: [web_search, web_fetch]fetch_content: true

Same as search, but after collecting results the pipeline ranks all URLs by relevance, fetches the top 5 full pages, runs a per-page summarizer, and uses the extracted facts for synthesis instead of raw snippets.

Best for agents where the article body matters — detailed reporting, analysis, documentation. Slower (~2× more LLM calls) but higher-quality output.

3. Direct feed

direct_sources:
- https://example.com/feed?date={today}
- https://api.example.com/poststool_bindings: [web_fetch] # required — or omit tool_bindings entirely

Skips the LLM planner entirely. Each URL is fetched directly (with {today} substituted as YYYY-MM-DD). Results go straight into the research buffer.

Best for RSS feeds, known API endpoints, or structured data pages updated on a schedule. Note: tool_bindings must include web_fetch or be empty — the default ["web_search"] will silently skip direct URLs.

4. Domain-biased search

sources:
- sfchronicle.com
- missionlocal.org

The LLM planner is given sources as a hint — it generates queries biased toward those domains but still searches the open web. Not a hard filter.

Best for agents that should prefer specific publications without being locked to them.

5. Multi-track

sub_queries:
- label: researchgoal: "Find recent ML papers about {query}"sources: [arxiv.org, paperswithcode.com]query_count: 3freshness: w
- label: industrygoal: "Find business news about {query} in AI"sources: [techcrunch.com, venturebeat.com]query_count: 3

N independent search pipelines run in parallel, each with its own goal, sources, freshness, and query count. Results are aggregated into a final synthesizer call.

Best for agents covering multiple distinct topics that benefit from isolated query sets (e.g., AI papers vs industry news vs tooling). Note: fetch_content and direct_sources are not inherited by sub-pipelines — each track is search-only.

Options that apply across all modes

validate_links — HEAD-checks every result URL and drops dead ones before synthesis. Adds a few seconds, improves source quality.

max_validator_loops — the validator judges whether the research buffer is sufficient. If not, it generates retry queries and reruns the search phase. Default 1 means one pass (no retry). Set to 23 for agents where completeness matters more than speed.

LLM providers

Configure via the Settings UI (gear icon) or set in .env. Cloud providers automatically use larger context windows and higher parallelism than local Ollama.

ProviderPROVIDER=Recommended models
Ollama (default)ollamaqwen3 / qwen3:1.7b (fast)
OpenAIopenaigpt-4.1 / gpt-4.1-mini (fast)
Anthropicanthropicclaude-sonnet-4-6 / claude-haiku-4-5-20251001 (fast)
Fireworksfireworksllama-v3p3-70b-instruct / llama-v3p1-8b-instruct (fast)

Set MODEL for the primary model and optionally FAST_MODEL for a smaller model used in the Planner/Validator phases.

Search providers

Web search fans out across multiple providers in parallel, merges and deduplicates results. DuckDuckGo works out of the box — no key required. Add any of the following free-tier providers to expand coverage:

ProviderKeyFree tierNotes
Ollama CloudOLLAMA_API_KEYollama.comRecommended; primary fan-out provider
Brave SearchBRAVE_API_KEY2K queries/month — brave.com/search/apiGood general coverage
Jina SearchJINA_API_KEYjina.aiAlso enables Jina Reader for page fetching; site: queries excluded
DuckDuckGononealways-onBaseline; 60s circuit-breaker, auto-recovers

Page fetching uses requests with automatic fallback to Jina Reader (r.jina.ai) when a page returns thin content — catches JavaScript-rendered shells transparently. Set WEB_FETCH_PROVIDER=jina to always use Jina.

Environment variables

Required (set in .env before first run)

These are read at startup before the UI is accessible and cannot be changed via the Settings UI.

VariableDefaultDescription
SERVER_API_KEYunsetBearer token for the HTTP API. Set this if the server is network-accessible — without it anyone who can reach the host can trigger runs and read your data. Not needed for localhost use.
SERVER_HOST0.0.0.0Interface to bind
SERVER_PORT8000Port to listen on

Configurable in the Settings UI

Open the gear icon in the web UI to change these at any time. A server restart is required for changes to take effect. See .env.example for the full list with comments.

VariableDefaultDescription
PROVIDERollamaLLM backend: ollama, openai, anthropic, fireworks
MODELqwen3Primary model for synthesis
FAST_MODELunsetSmaller model for Planner/Validator phases; falls back to MODEL
OLLAMA_BASE_URLhttp://localhost:11434Ollama server address
OPENAI_API_KEYRequired if PROVIDER=openai
ANTHROPIC_API_KEYRequired if PROVIDER=anthropic
FIREWORKS_API_KEYRequired if PROVIDER=fireworks
BRAVE_API_KEYBrave Search — 2K queries/month free
JINA_API_KEYJina Search + higher Reader rate limits
LOCATIONunsetCity/region injected into planner prompt for location-aware searches
CONTEXT_BUDGETcloudcloud / local / minimal — controls content truncation for local models
TEMPERATURE0.2LLM sampling temperature
TIMEOUT300LLM request timeout in seconds

Not in Settings UI (.env only)

VariableDefaultDescription
OLLAMA_API_KEYOllama Cloud search key — ollama.com
WEB_FETCH_PROVIDERhttphttp (raw fetch + Jina fallback) or jina (always use Jina Reader)
DISPATCH_DB_PATHsearch_history.dbSQLite path
DISPATCH_AGENTS_DIR./agentsYAML seed files directory
PLANNER_QUERY_COUNT5Global default for planner_query_count (overridden per-agent)
PARALLEL_SUB_PIPELINES1Max parallel workers for multi-track pipelines; 0 = sequential

Web UI

The web UI (/) is a single-page app. The main area shows the current conversation; the sidebar manages agents and schedules.

Tabs

Digest — a compiled briefing assembled from the most recent run of each active agent. Skips agents that haven't run yet.

Active — all enabled dispatches (agent + prompt + schedule). Each card shows the agent pill, label, next run time, and last run status. Click a card to view the last output, edit the schedule, or run it immediately.

Archived — disabled dispatches. Re-enable from here without losing the schedule.

Library — all agents in the database, whether scheduled or not. Browse, create, edit, and export agents here.

Agent type

When creating or editing an agent, select the agent type first:

  • Web Search — the planner generates queries and searches the web. Configure preferred domains, fetch content, and freshness filter.
  • Direct Fetch — provide a list of URLs to fetch directly (no search). {today} is substituted with the current date. Best for feeds and known API endpoints.

Tool bindings are inferred automatically from the type and don't need to be set manually.

Creating and scheduling an agent

  1. Library → New agent — give it a name, label, goal template, sources, and optionally a color swatch
  2. Save — the agent appears in the Library table immediately
  3. Click Schedule on the new row — the dispatch sheet opens with the agent pre-selected
  4. Enter the prompt the agent will run with (e.g. "SF local news digest"), set frequency and time, save
  5. The agent appears in Active and will run on schedule

Schedule Dispatch

Editing agents

Accessible from Library → Edit or by clicking a row. Fields map directly to the YAML schema above. Changes take effect on the next run without a server restart.

Settings

The gear icon (top-right) opens the Settings sheet. Changes are saved to .env and take effect after a server restart.

SectionWhat you can configure
LLM ProviderProvider, model, API key, Ollama base URL, fast model
SearchOllama Cloud, Brave, and Jina API keys
PersonalizationLocation (used by local-aware agents)
AdvancedContext budget, temperature, LLM timeout

Other UI features

  • + New — start a one-off prompt against any agent (no schedule)
  • think — toggle Qwen3 reasoning mode for the current prompt
  • Stop — cancel a running job; shows live elapsed time while running
  • Responses rendered as markdown with clickable links

CLI

python main.py --serve # start server
python main.py --agent news # interactive
python main.py --agent news --once # run once and exit
python main.py --agent apartment --once --query "2br sf soma under 3000"

Scheduling via system crontab (alternative to UI schedules)

0 7 ***cd /path/to/dispatch && python main.py --agent my-agent --once
0 8 ** 1 cd /path/to/dispatch && python main.py --agent my-agent --once --query "weekly recap"

Deploying as a service

A systemd service file is included at deploy/dispatch.service. Copy it to /etc/systemd/system/, update the paths, and:

sudo systemctl enable dispatch
sudo systemctl start dispatch

The server reads .env from the working directory and auto-restarts on failure.

Tools

ToolDescription
web_searchFan-out search across Ollama, Brave, Jina, and DDG in parallel; results merged and deduplicated
web_fetchFetch full page content from a URL; falls back to Jina Reader for JS-rendered or thin pages

Files

main.py # CLI entry point and dispatcher
pipeline.py # AgentPipeline and MultiAgentPipeline orchestrators
agent_loader.py # loads DB agent rows → DomainConfig objects at runtime
agent_config.py # DomainConfig and SubQueryConfig dataclasses
agents/ # optional YAML files — import via UI or POST /api/agents/import
server.py # FastAPI HTTP server and web UI
db.py # SQLite schema, CRUD, and YAML seed loader
tools.py # Tool definitions (web_search, web_fetch) and dispatch
ui.html # single-page web UI (served at /)
logs.py # CLI tool for reviewing session history
requirements.txt
.env.example
deploy/ # systemd service file
search_history.db # created on first run (gitignore this)

SQLite schema

sessions (id, model, agent, source, started_at, ended_at, viewed_at, total_duration_ms)
messages (id, session_id, role, content, created_at)
tool_calls (id, session_id, tool_name, args, result, duration_ms, created_at)
dispatches (id, agent, prompt, schedule, label, enabled, run_on_create, created_at,
trigger_type, repeat_until, max_runs, run_count)
agents (id, name, label, description, enabled, color,
goal_template, output_template,
sources, sub_queries, tool_bindings, direct_sources,
freshness, planner_query_count, max_validator_loops,
validate_links, fetch_content,
is_builtin, created_at, updated_at)
settings (key TEXTPRIMARY KEY, value TEXT, updated_at TEXT)

source in sessions is cli, http, or cron. role in messages is user, assistant, or error.

dispatches.schedule is a standard 5-field cron expression. Schedules are managed by APScheduler inside the server process and persist across restarts.

agents.color is a hue integer (0–359). Auto-assigned randomly when an agent is first created if not specified in YAML.

Querying history

CLI (logs.py)

python logs.py # last 50 sessions
python logs.py -n 100 # last 100 sessions
python logs.py --agent news # filter by agent
python logs.py --session 42 # full detail for session 42

Direct SQLite

-- Recent sessions with first promptSELECTs.id, s.agent, s.started_at,
(SELECT content FROM messages WHERE session_id=s.idAND role='user'ORDER BY id LIMIT1) AS prompt
FROM sessions s ORDER BYs.idDESCLIMIT20;
-- Search by keywordSELECT*FROM messages WHERE content LIKE'%keyword%';
-- Tool calls for a sessionSELECT tool_name, args, result FROM tool_calls WHERE session_id=42;

Python (db.py)

importdbsessions=db.get_sessions(limit=50, agent="news")
detail=db.get_session_detail(42)
results=db.search_sessions(agent="news", keyword="market")

HTTP API

GET / web UI
GET /health sanity check
POST /api/jobs submit a job (async)
GET /api/jobs/{job_id} poll status / result
DELETE /api/jobs/{job_id} cancel a running job
GET /api/sessions list sessions (?agent=news&limit=50)
GET /api/sessions/{id} full session detail (messages + tool calls with duration)
GET /api/dispatches list dispatches
POST /api/dispatches create a dispatch
PUT /api/dispatches/{id} update a dispatch
DELETE /api/dispatches/{id} delete a dispatch
GET /api/agents list all agents
GET /api/agents/{name} full agent detail
POST /api/agents create an agent
PUT /api/agents/{name} update an agent
DELETE /api/agents/{name} delete a non-builtin agent
GET /api/agents/{name}/export export agent as YAML
POST /api/agents/import create/update agent from YAML body
GET /api/tools list available tools
GET /api/settings current settings (values from live environment)
POST /api/settings save settings to DB + .env (restart required to apply)
POST /v1/chat/completions OpenAI-compatible (synchronous)
GET /v1/models

Authentication

python -c "import secrets; print(secrets.token_urlsafe(32))"

Set as SERVER_API_KEY=... in .env. If unset, all requests are accepted.

Note: When SERVER_API_KEY is set, the server injects it into the web UI's HTML on page load so the frontend can authenticate its API calls automatically. This means the key is visible in the browser's View Source on the / route. Keep the server on a trusted network (localhost, Tailscale, private LAN) — don't bind to a public interface without additional network-level access controls.

License

MIT — see LICENSE.

About

Self-hosted AI research agent platform. Define agents, schedule them as dispatches, and get synthesized digests — no manual searching. Bring your own LLM (Ollama, OpenAI, Anthropic, Fireworks).

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

dispatch

License: MIT

Self-hosted AI research agent platform. Define agents for topics you care about, schedule them as dispatches, and get synthesized digests — no manual searching. Runs on your machine or server. Bring your own LLM (Ollama, OpenAI, Anthropic, Fireworks).

Read Digest

What you get

  • Web UI to browse, schedule, and read agent output — no config files required
  • Daily digest compiled from the most recent run of each active agent
  • Multi-track pipelines that fan out searches across independent topic tracks in parallel

Quick Start

# 1. Create and activate a virtual environment
python -m venv dispatch-env
source dispatch-env/bin/activate
# 2. Install dependencies
pip install -r requirements.txt
# 3. Configure environment
cp .env.example .env
# Set SERVER_API_KEY if the server will be network-accessible (see .env.example)# Everything else — provider, model, API keys, location — can be set in the Settings UI# 4. Pull a model (Ollama only — skip if using a cloud provider)
ollama pull qwen3
# 5. Start the server
python main.py --serve # http://localhost:8000

Open the web UI, go to Library, create your first agent, then click Schedule to activate it.

How agents work

There are two distinct concepts: agents and dispatches.

  • Agent — what an agent is: its name, goal, sources, search settings, and color. Agents live in the database and are the source of truth for behavior.
  • Dispatch — an agent paired with a user prompt and a schedule. One agent can have multiple dispatches (e.g., the news agent running daily at 7am with a general prompt and again on Fridays with a weekly-recap prompt).

No agents are active by default. Create agents in the Library tab, then activate them by creating a dispatch.

Library tab — create and browse agents
↓
Schedule button → creates a dispatch (agent + prompt + schedule)
↓
Active tab — running scheduled dispatches

How it works

Each agent run uses a multi-phase micro-agent pipeline. Each phase is a small, focused LLM call with a constrained tool budget. The orchestrator (Python) manages state between phases; no single model call holds the full context.

query
→ Planner (LLM) — generate N search queries (or skip if direct_sources set)
→ Searcher (API) — fire all queries in parallel, collect results
→ Validator (LLM) — coverage sufficient? retry with new queries if not
→ Synthesizer (LLM) — format final answer from research buffer

Optional phases (enabled per-agent via config):

  • Link validator — HEAD-checks result URLs, drops dead ones before synthesis
  • Ranker + Fetcher — LLM selects top URLs → parallel web_fetch → per-page summarizer

Adding a new agent

Option 1 — Web UI

Configure Agent

Library tab → New agent → fill in name, goal, sources, and color → Save. The agent immediately appears in the Library and can be scheduled.

Option 2 — YAML

Drop .yaml files into your agents directory (defaults to ./agents, override with DISPATCH_AGENTS_DIR in .env). No agents folder is included in the repo — create it and add your own files.

Create agents/my-agent.yaml:

name: my-agentlabel: My Agentdescription: What this agent covers.color: 175# hue 0–359; omit to auto-assign on first seedgoal_template: "Find recent {query}. Focus on X, Y, Z."output_template: "Return a bulleted digest with source links."sources:
- example.com
- anothersource.orgtool_bindings:
- web_searchplanner_query_count: 5freshness: w # d=24h w=7d m=30d y=1y (omit = any date)

Restart the server — the agent appears in the Library tab. Click Schedule to activate it.

Option 3 — Import YAML

Library tab → Import or POST /api/agents/import with a YAML body. Useful for migrating agents between instances.

YAML field reference

FieldTypeDefaultDescription
namestringrequiredUnique slug — used as the agent identifier everywhere
labelstring""Display name shown in pills and headers
descriptionstring""Short summary shown in the Library table
colorintegerrandomHue (0–359) for the color pill. Auto-assigned on first seed if omitted; never overwritten by re-seeding
goal_templatestringrequiredLLM prompt for what to find. {query} → user input, {today} → YYYY-MM-DD
output_templatestring""Instructions for how the synthesizer should format the response
sourceslist[]Domains to bias planner queries toward (e.g. sfchronicle.com)
direct_sourceslist[]URLs fetched directly, bypassing the planner. {today} substitution supported
sub_querieslistnullMulti-track pipelines — each track runs an isolated search pass (see Pipeline modes)
tool_bindingslist["web_search"]Tools available to this agent. Empty list = all tools
freshnessstringnullDate filter: dwmy
planner_query_countinteger5Number of search queries the planner generates
max_validator_loopsinteger1How many times the validator can request additional searches
validate_linksboolfalseHEAD-check each result URL and drop dead ones before synthesis
fetch_contentboolfalseRank URLs by relevance, fetch top 5 full pages, summarize per page

Pipeline modes

The pipeline is selected automatically based on your config. All modes end with a Validator loop and a Synthesizer call.

1. Search (default)

# no direct_sourcestool_bindings: [web_search]

LLM planner generates planner_query_count queries → parallel web search across all providers → validate → synthesize.

Best for news, research, any topic where broad query coverage helps. sources narrows the planner's focus toward specific domains without locking it to them.

2. Search + deep read

tool_bindings: [web_search, web_fetch]fetch_content: true

Same as search, but after collecting results the pipeline ranks all URLs by relevance, fetches the top 5 full pages, runs a per-page summarizer, and uses the extracted facts for synthesis instead of raw snippets.

Best for agents where the article body matters — detailed reporting, analysis, documentation. Slower (~2× more LLM calls) but higher-quality output.

3. Direct feed

direct_sources:
- https://example.com/feed?date={today}
- https://api.example.com/poststool_bindings: [web_fetch] # required — or omit tool_bindings entirely

Skips the LLM planner entirely. Each URL is fetched directly (with {today} substituted as YYYY-MM-DD). Results go straight into the research buffer.

Best for RSS feeds, known API endpoints, or structured data pages updated on a schedule. Note: tool_bindings must include web_fetch or be empty — the default ["web_search"] will silently skip direct URLs.

4. Domain-biased search

sources:
- sfchronicle.com
- missionlocal.org

The LLM planner is given sources as a hint — it generates queries biased toward those domains but still searches the open web. Not a hard filter.

Best for agents that should prefer specific publications without being locked to them.

5. Multi-track

sub_queries:
- label: researchgoal: "Find recent ML papers about {query}"sources: [arxiv.org, paperswithcode.com]query_count: 3freshness: w
- label: industrygoal: "Find business news about {query} in AI"sources: [techcrunch.com, venturebeat.com]query_count: 3

N independent search pipelines run in parallel, each with its own goal, sources, freshness, and query count. Results are aggregated into a final synthesizer call.

Best for agents covering multiple distinct topics that benefit from isolated query sets (e.g., AI papers vs industry news vs tooling). Note: fetch_content and direct_sources are not inherited by sub-pipelines — each track is search-only.

Options that apply across all modes

validate_links — HEAD-checks every result URL and drops dead ones before synthesis. Adds a few seconds, improves source quality.

max_validator_loops — the validator judges whether the research buffer is sufficient. If not, it generates retry queries and reruns the search phase. Default 1 means one pass (no retry). Set to 23 for agents where completeness matters more than speed.

LLM providers

Configure via the Settings UI (gear icon) or set in .env. Cloud providers automatically use larger context windows and higher parallelism than local Ollama.

ProviderPROVIDER=Recommended models
Ollama (default)ollamaqwen3 / qwen3:1.7b (fast)
OpenAIopenaigpt-4.1 / gpt-4.1-mini (fast)
Anthropicanthropicclaude-sonnet-4-6 / claude-haiku-4-5-20251001 (fast)
Fireworksfireworksllama-v3p3-70b-instruct / llama-v3p1-8b-instruct (fast)

Set MODEL for the primary model and optionally FAST_MODEL for a smaller model used in the Planner/Validator phases.

Search providers

Web search fans out across multiple providers in parallel, merges and deduplicates results. DuckDuckGo works out of the box — no key required. Add any of the following free-tier providers to expand coverage:

ProviderKeyFree tierNotes
Ollama CloudOLLAMA_API_KEYollama.comRecommended; primary fan-out provider
Brave SearchBRAVE_API_KEY2K queries/month — brave.com/search/apiGood general coverage
Jina SearchJINA_API_KEYjina.aiAlso enables Jina Reader for page fetching; site: queries excluded
DuckDuckGononealways-onBaseline; 60s circuit-breaker, auto-recovers

Page fetching uses requests with automatic fallback to Jina Reader (r.jina.ai) when a page returns thin content — catches JavaScript-rendered shells transparently. Set WEB_FETCH_PROVIDER=jina to always use Jina.

Environment variables

Required (set in .env before first run)

These are read at startup before the UI is accessible and cannot be changed via the Settings UI.

VariableDefaultDescription
SERVER_API_KEYunsetBearer token for the HTTP API. Set this if the server is network-accessible — without it anyone who can reach the host can trigger runs and read your data. Not needed for localhost use.
SERVER_HOST0.0.0.0Interface to bind
SERVER_PORT8000Port to listen on

Configurable in the Settings UI

Open the gear icon in the web UI to change these at any time. A server restart is required for changes to take effect. See .env.example for the full list with comments.

VariableDefaultDescription
PROVIDERollamaLLM backend: ollama, openai, anthropic, fireworks
MODELqwen3Primary model for synthesis
FAST_MODELunsetSmaller model for Planner/Validator phases; falls back to MODEL
OLLAMA_BASE_URLhttp://localhost:11434Ollama server address
OPENAI_API_KEYRequired if PROVIDER=openai
ANTHROPIC_API_KEYRequired if PROVIDER=anthropic
FIREWORKS_API_KEYRequired if PROVIDER=fireworks
BRAVE_API_KEYBrave Search — 2K queries/month free
JINA_API_KEYJina Search + higher Reader rate limits
LOCATIONunsetCity/region injected into planner prompt for location-aware searches
CONTEXT_BUDGETcloudcloud / local / minimal — controls content truncation for local models
TEMPERATURE0.2LLM sampling temperature
TIMEOUT300LLM request timeout in seconds

Not in Settings UI (.env only)

VariableDefaultDescription
OLLAMA_API_KEYOllama Cloud search key — ollama.com
WEB_FETCH_PROVIDERhttphttp (raw fetch + Jina fallback) or jina (always use Jina Reader)
DISPATCH_DB_PATHsearch_history.dbSQLite path
DISPATCH_AGENTS_DIR./agentsYAML seed files directory
PLANNER_QUERY_COUNT5Global default for planner_query_count (overridden per-agent)
PARALLEL_SUB_PIPELINES1Max parallel workers for multi-track pipelines; 0 = sequential

Web UI

The web UI (/) is a single-page app. The main area shows the current conversation; the sidebar manages agents and schedules.

Tabs

Digest — a compiled briefing assembled from the most recent run of each active agent. Skips agents that haven't run yet.

Active — all enabled dispatches (agent + prompt + schedule). Each card shows the agent pill, label, next run time, and last run status. Click a card to view the last output, edit the schedule, or run it immediately.

Archived — disabled dispatches. Re-enable from here without losing the schedule.

Library — all agents in the database, whether scheduled or not. Browse, create, edit, and export agents here.

Agent type

When creating or editing an agent, select the agent type first:

  • Web Search — the planner generates queries and searches the web. Configure preferred domains, fetch content, and freshness filter.
  • Direct Fetch — provide a list of URLs to fetch directly (no search). {today} is substituted with the current date. Best for feeds and known API endpoints.

Tool bindings are inferred automatically from the type and don't need to be set manually.

Creating and scheduling an agent

  1. Library → New agent — give it a name, label, goal template, sources, and optionally a color swatch
  2. Save — the agent appears in the Library table immediately
  3. Click Schedule on the new row — the dispatch sheet opens with the agent pre-selected
  4. Enter the prompt the agent will run with (e.g. "SF local news digest"), set frequency and time, save
  5. The agent appears in Active and will run on schedule

Schedule Dispatch

Editing agents

Accessible from Library → Edit or by clicking a row. Fields map directly to the YAML schema above. Changes take effect on the next run without a server restart.

Settings

The gear icon (top-right) opens the Settings sheet. Changes are saved to .env and take effect after a server restart.

SectionWhat you can configure
LLM ProviderProvider, model, API key, Ollama base URL, fast model
SearchOllama Cloud, Brave, and Jina API keys
PersonalizationLocation (used by local-aware agents)
AdvancedContext budget, temperature, LLM timeout

Other UI features

  • + New — start a one-off prompt against any agent (no schedule)
  • think — toggle Qwen3 reasoning mode for the current prompt
  • Stop — cancel a running job; shows live elapsed time while running
  • Responses rendered as markdown with clickable links

CLI

python main.py --serve # start server
python main.py --agent news # interactive
python main.py --agent news --once # run once and exit
python main.py --agent apartment --once --query "2br sf soma under 3000"

Scheduling via system crontab (alternative to UI schedules)

0 7 ***cd /path/to/dispatch && python main.py --agent my-agent --once
0 8 ** 1 cd /path/to/dispatch && python main.py --agent my-agent --once --query "weekly recap"

Deploying as a service

A systemd service file is included at deploy/dispatch.service. Copy it to /etc/systemd/system/, update the paths, and:

sudo systemctl enable dispatch
sudo systemctl start dispatch

The server reads .env from the working directory and auto-restarts on failure.

Tools

ToolDescription
web_searchFan-out search across Ollama, Brave, Jina, and DDG in parallel; results merged and deduplicated
web_fetchFetch full page content from a URL; falls back to Jina Reader for JS-rendered or thin pages

Files

main.py # CLI entry point and dispatcher
pipeline.py # AgentPipeline and MultiAgentPipeline orchestrators
agent_loader.py # loads DB agent rows → DomainConfig objects at runtime
agent_config.py # DomainConfig and SubQueryConfig dataclasses
agents/ # optional YAML files — import via UI or POST /api/agents/import
server.py # FastAPI HTTP server and web UI
db.py # SQLite schema, CRUD, and YAML seed loader
tools.py # Tool definitions (web_search, web_fetch) and dispatch
ui.html # single-page web UI (served at /)
logs.py # CLI tool for reviewing session history
requirements.txt
.env.example
deploy/ # systemd service file
search_history.db # created on first run (gitignore this)

SQLite schema

sessions (id, model, agent, source, started_at, ended_at, viewed_at, total_duration_ms)
messages (id, session_id, role, content, created_at)
tool_calls (id, session_id, tool_name, args, result, duration_ms, created_at)
dispatches (id, agent, prompt, schedule, label, enabled, run_on_create, created_at,
trigger_type, repeat_until, max_runs, run_count)
agents (id, name, label, description, enabled, color,
goal_template, output_template,
sources, sub_queries, tool_bindings, direct_sources,
freshness, planner_query_count, max_validator_loops,
validate_links, fetch_content,
is_builtin, created_at, updated_at)
settings (key TEXTPRIMARY KEY, value TEXT, updated_at TEXT)

source in sessions is cli, http, or cron. role in messages is user, assistant, or error.

dispatches.schedule is a standard 5-field cron expression. Schedules are managed by APScheduler inside the server process and persist across restarts.

agents.color is a hue integer (0–359). Auto-assigned randomly when an agent is first created if not specified in YAML.

Querying history

CLI (logs.py)

python logs.py # last 50 sessions
python logs.py -n 100 # last 100 sessions
python logs.py --agent news # filter by agent
python logs.py --session 42 # full detail for session 42

Direct SQLite

-- Recent sessions with first promptSELECTs.id, s.agent, s.started_at,
(SELECT content FROM messages WHERE session_id=s.idAND role='user'ORDER BY id LIMIT1) AS prompt
FROM sessions s ORDER BYs.idDESCLIMIT20;
-- Search by keywordSELECT*FROM messages WHERE content LIKE'%keyword%';
-- Tool calls for a sessionSELECT tool_name, args, result FROM tool_calls WHERE session_id=42;

Python (db.py)

importdbsessions=db.get_sessions(limit=50, agent="news")
detail=db.get_session_detail(42)
results=db.search_sessions(agent="news", keyword="market")

HTTP API

GET / web UI
GET /health sanity check
POST /api/jobs submit a job (async)
GET /api/jobs/{job_id} poll status / result
DELETE /api/jobs/{job_id} cancel a running job
GET /api/sessions list sessions (?agent=news&limit=50)
GET /api/sessions/{id} full session detail (messages + tool calls with duration)
GET /api/dispatches list dispatches
POST /api/dispatches create a dispatch
PUT /api/dispatches/{id} update a dispatch
DELETE /api/dispatches/{id} delete a dispatch
GET /api/agents list all agents
GET /api/agents/{name} full agent detail
POST /api/agents create an agent
PUT /api/agents/{name} update an agent
DELETE /api/agents/{name} delete a non-builtin agent
GET /api/agents/{name}/export export agent as YAML
POST /api/agents/import create/update agent from YAML body
GET /api/tools list available tools
GET /api/settings current settings (values from live environment)
POST /api/settings save settings to DB + .env (restart required to apply)
POST /v1/chat/completions OpenAI-compatible (synchronous)
GET /v1/models

Authentication

python -c "import secrets; print(secrets.token_urlsafe(32))"

Set as SERVER_API_KEY=... in .env. If unset, all requests are accepted.

Note: When SERVER_API_KEY is set, the server injects it into the web UI's HTML on page load so the frontend can authenticate its API calls automatically. This means the key is visible in the browser's View Source on the / route. Keep the server on a trusted network (localhost, Tailscale, private LAN) — don't bind to a public interface without additional network-level access controls.

License

MIT — see LICENSE.

About

Self-hosted AI research agent platform. Define agents, schedule them as dispatches, and get synthesized digests — no manual searching. Bring your own LLM (Ollama, OpenAI, Anthropic, Fireworks).

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

dispatch

License: MIT

Self-hosted AI research agent platform. Define agents for topics you care about, schedule them as dispatches, and get synthesized digests — no manual searching. Runs on your machine or server. Bring your own LLM (Ollama, OpenAI, Anthropic, Fireworks).

Read Digest

What you get

  • Web UI to browse, schedule, and read agent output — no config files required
  • Daily digest compiled from the most recent run of each active agent
  • Multi-track pipelines that fan out searches across independent topic tracks in parallel

Quick Start

# 1. Create and activate a virtual environment
python -m venv dispatch-env
source dispatch-env/bin/activate
# 2. Install dependencies
pip install -r requirements.txt
# 3. Configure environment
cp .env.example .env
# Set SERVER_API_KEY if the server will be network-accessible (see .env.example)# Everything else — provider, model, API keys, location — can be set in the Settings UI# 4. Pull a model (Ollama only — skip if using a cloud provider)
ollama pull qwen3
# 5. Start the server
python main.py --serve # http://localhost:8000

Open the web UI, go to Library, create your first agent, then click Schedule to activate it.

How agents work

There are two distinct concepts: agents and dispatches.

  • Agent — what an agent is: its name, goal, sources, search settings, and color. Agents live in the database and are the source of truth for behavior.
  • Dispatch — an agent paired with a user prompt and a schedule. One agent can have multiple dispatches (e.g., the news agent running daily at 7am with a general prompt and again on Fridays with a weekly-recap prompt).

No agents are active by default. Create agents in the Library tab, then activate them by creating a dispatch.

Library tab — create and browse agents
↓
Schedule button → creates a dispatch (agent + prompt + schedule)
↓
Active tab — running scheduled dispatches

How it works

Each agent run uses a multi-phase micro-agent pipeline. Each phase is a small, focused LLM call with a constrained tool budget. The orchestrator (Python) manages state between phases; no single model call holds the full context.

query
→ Planner (LLM) — generate N search queries (or skip if direct_sources set)
→ Searcher (API) — fire all queries in parallel, collect results
→ Validator (LLM) — coverage sufficient? retry with new queries if not
→ Synthesizer (LLM) — format final answer from research buffer

Optional phases (enabled per-agent via config):

  • Link validator — HEAD-checks result URLs, drops dead ones before synthesis
  • Ranker + Fetcher — LLM selects top URLs → parallel web_fetch → per-page summarizer

Adding a new agent

Option 1 — Web UI

Configure Agent

Library tab → New agent → fill in name, goal, sources, and color → Save. The agent immediately appears in the Library and can be scheduled.

Option 2 — YAML

Drop .yaml files into your agents directory (defaults to ./agents, override with DISPATCH_AGENTS_DIR in .env). No agents folder is included in the repo — create it and add your own files.

Create agents/my-agent.yaml:

name: my-agentlabel: My Agentdescription: What this agent covers.color: 175# hue 0–359; omit to auto-assign on first seedgoal_template: "Find recent {query}. Focus on X, Y, Z."output_template: "Return a bulleted digest with source links."sources:
- example.com
- anothersource.orgtool_bindings:
- web_searchplanner_query_count: 5freshness: w # d=24h w=7d m=30d y=1y (omit = any date)

Restart the server — the agent appears in the Library tab. Click Schedule to activate it.

Option 3 — Import YAML

Library tab → Import or POST /api/agents/import with a YAML body. Useful for migrating agents between instances.

YAML field reference

FieldTypeDefaultDescription
namestringrequiredUnique slug — used as the agent identifier everywhere
labelstring""Display name shown in pills and headers
descriptionstring""Short summary shown in the Library table
colorintegerrandomHue (0–359) for the color pill. Auto-assigned on first seed if omitted; never overwritten by re-seeding
goal_templatestringrequiredLLM prompt for what to find. {query} → user input, {today} → YYYY-MM-DD
output_templatestring""Instructions for how the synthesizer should format the response
sourceslist[]Domains to bias planner queries toward (e.g. sfchronicle.com)
direct_sourceslist[]URLs fetched directly, bypassing the planner. {today} substitution supported
sub_querieslistnullMulti-track pipelines — each track runs an isolated search pass (see Pipeline modes)
tool_bindingslist["web_search"]Tools available to this agent. Empty list = all tools
freshnessstringnullDate filter: dwmy
planner_query_countinteger5Number of search queries the planner generates
max_validator_loopsinteger1How many times the validator can request additional searches
validate_linksboolfalseHEAD-check each result URL and drop dead ones before synthesis
fetch_contentboolfalseRank URLs by relevance, fetch top 5 full pages, summarize per page

Pipeline modes

The pipeline is selected automatically based on your config. All modes end with a Validator loop and a Synthesizer call.

1. Search (default)

# no direct_sourcestool_bindings: [web_search]

LLM planner generates planner_query_count queries → parallel web search across all providers → validate → synthesize.

Best for news, research, any topic where broad query coverage helps. sources narrows the planner's focus toward specific domains without locking it to them.

2. Search + deep read

tool_bindings: [web_search, web_fetch]fetch_content: true

Same as search, but after collecting results the pipeline ranks all URLs by relevance, fetches the top 5 full pages, runs a per-page summarizer, and uses the extracted facts for synthesis instead of raw snippets.

Best for agents where the article body matters — detailed reporting, analysis, documentation. Slower (~2× more LLM calls) but higher-quality output.

3. Direct feed

direct_sources:
- https://example.com/feed?date={today}
- https://api.example.com/poststool_bindings: [web_fetch] # required — or omit tool_bindings entirely

Skips the LLM planner entirely. Each URL is fetched directly (with {today} substituted as YYYY-MM-DD). Results go straight into the research buffer.

Best for RSS feeds, known API endpoints, or structured data pages updated on a schedule. Note: tool_bindings must include web_fetch or be empty — the default ["web_search"] will silently skip direct URLs.

4. Domain-biased search

sources:
- sfchronicle.com
- missionlocal.org

The LLM planner is given sources as a hint — it generates queries biased toward those domains but still searches the open web. Not a hard filter.

Best for agents that should prefer specific publications without being locked to them.

5. Multi-track

sub_queries:
- label: researchgoal: "Find recent ML papers about {query}"sources: [arxiv.org, paperswithcode.com]query_count: 3freshness: w
- label: industrygoal: "Find business news about {query} in AI"sources: [techcrunch.com, venturebeat.com]query_count: 3

N independent search pipelines run in parallel, each with its own goal, sources, freshness, and query count. Results are aggregated into a final synthesizer call.

Best for agents covering multiple distinct topics that benefit from isolated query sets (e.g., AI papers vs industry news vs tooling). Note: fetch_content and direct_sources are not inherited by sub-pipelines — each track is search-only.

Options that apply across all modes

validate_links — HEAD-checks every result URL and drops dead ones before synthesis. Adds a few seconds, improves source quality.

max_validator_loops — the validator judges whether the research buffer is sufficient. If not, it generates retry queries and reruns the search phase. Default 1 means one pass (no retry). Set to 23 for agents where completeness matters more than speed.

LLM providers

Configure via the Settings UI (gear icon) or set in .env. Cloud providers automatically use larger context windows and higher parallelism than local Ollama.

ProviderPROVIDER=Recommended models
Ollama (default)ollamaqwen3 / qwen3:1.7b (fast)
OpenAIopenaigpt-4.1 / gpt-4.1-mini (fast)
Anthropicanthropicclaude-sonnet-4-6 / claude-haiku-4-5-20251001 (fast)
Fireworksfireworksllama-v3p3-70b-instruct / llama-v3p1-8b-instruct (fast)

Set MODEL for the primary model and optionally FAST_MODEL for a smaller model used in the Planner/Validator phases.

Search providers

Web search fans out across multiple providers in parallel, merges and deduplicates results. DuckDuckGo works out of the box — no key required. Add any of the following free-tier providers to expand coverage:

ProviderKeyFree tierNotes
Ollama CloudOLLAMA_API_KEYollama.comRecommended; primary fan-out provider
Brave SearchBRAVE_API_KEY2K queries/month — brave.com/search/apiGood general coverage
Jina SearchJINA_API_KEYjina.aiAlso enables Jina Reader for page fetching; site: queries excluded
DuckDuckGononealways-onBaseline; 60s circuit-breaker, auto-recovers

Page fetching uses requests with automatic fallback to Jina Reader (r.jina.ai) when a page returns thin content — catches JavaScript-rendered shells transparently. Set WEB_FETCH_PROVIDER=jina to always use Jina.

Environment variables

Required (set in .env before first run)

These are read at startup before the UI is accessible and cannot be changed via the Settings UI.

VariableDefaultDescription
SERVER_API_KEYunsetBearer token for the HTTP API. Set this if the server is network-accessible — without it anyone who can reach the host can trigger runs and read your data. Not needed for localhost use.
SERVER_HOST0.0.0.0Interface to bind
SERVER_PORT8000Port to listen on

Configurable in the Settings UI

Open the gear icon in the web UI to change these at any time. A server restart is required for changes to take effect. See .env.example for the full list with comments.

VariableDefaultDescription
PROVIDERollamaLLM backend: ollama, openai, anthropic, fireworks
MODELqwen3Primary model for synthesis
FAST_MODELunsetSmaller model for Planner/Validator phases; falls back to MODEL
OLLAMA_BASE_URLhttp://localhost:11434Ollama server address
OPENAI_API_KEYRequired if PROVIDER=openai
ANTHROPIC_API_KEYRequired if PROVIDER=anthropic
FIREWORKS_API_KEYRequired if PROVIDER=fireworks
BRAVE_API_KEYBrave Search — 2K queries/month free
JINA_API_KEYJina Search + higher Reader rate limits
LOCATIONunsetCity/region injected into planner prompt for location-aware searches
CONTEXT_BUDGETcloudcloud / local / minimal — controls content truncation for local models
TEMPERATURE0.2LLM sampling temperature
TIMEOUT300LLM request timeout in seconds

Not in Settings UI (.env only)

VariableDefaultDescription
OLLAMA_API_KEYOllama Cloud search key — ollama.com
WEB_FETCH_PROVIDERhttphttp (raw fetch + Jina fallback) or jina (always use Jina Reader)
DISPATCH_DB_PATHsearch_history.dbSQLite path
DISPATCH_AGENTS_DIR./agentsYAML seed files directory
PLANNER_QUERY_COUNT5Global default for planner_query_count (overridden per-agent)
PARALLEL_SUB_PIPELINES1Max parallel workers for multi-track pipelines; 0 = sequential

Web UI

The web UI (/) is a single-page app. The main area shows the current conversation; the sidebar manages agents and schedules.

Tabs

Digest — a compiled briefing assembled from the most recent run of each active agent. Skips agents that haven't run yet.

Active — all enabled dispatches (agent + prompt + schedule). Each card shows the agent pill, label, next run time, and last run status. Click a card to view the last output, edit the schedule, or run it immediately.

Archived — disabled dispatches. Re-enable from here without losing the schedule.

Library — all agents in the database, whether scheduled or not. Browse, create, edit, and export agents here.

Agent type

When creating or editing an agent, select the agent type first:

  • Web Search — the planner generates queries and searches the web. Configure preferred domains, fetch content, and freshness filter.
  • Direct Fetch — provide a list of URLs to fetch directly (no search). {today} is substituted with the current date. Best for feeds and known API endpoints.

Tool bindings are inferred automatically from the type and don't need to be set manually.

Creating and scheduling an agent

  1. Library → New agent — give it a name, label, goal template, sources, and optionally a color swatch
  2. Save — the agent appears in the Library table immediately
  3. Click Schedule on the new row — the dispatch sheet opens with the agent pre-selected
  4. Enter the prompt the agent will run with (e.g. "SF local news digest"), set frequency and time, save
  5. The agent appears in Active and will run on schedule

Schedule Dispatch

Editing agents

Accessible from Library → Edit or by clicking a row. Fields map directly to the YAML schema above. Changes take effect on the next run without a server restart.

Settings

The gear icon (top-right) opens the Settings sheet. Changes are saved to .env and take effect after a server restart.

SectionWhat you can configure
LLM ProviderProvider, model, API key, Ollama base URL, fast model
SearchOllama Cloud, Brave, and Jina API keys
PersonalizationLocation (used by local-aware agents)
AdvancedContext budget, temperature, LLM timeout

Other UI features

  • + New — start a one-off prompt against any agent (no schedule)
  • think — toggle Qwen3 reasoning mode for the current prompt
  • Stop — cancel a running job; shows live elapsed time while running
  • Responses rendered as markdown with clickable links

CLI

python main.py --serve # start server
python main.py --agent news # interactive
python main.py --agent news --once # run once and exit
python main.py --agent apartment --once --query "2br sf soma under 3000"

Scheduling via system crontab (alternative to UI schedules)

0 7 ***cd /path/to/dispatch && python main.py --agent my-agent --once
0 8 ** 1 cd /path/to/dispatch && python main.py --agent my-agent --once --query "weekly recap"

Deploying as a service

A systemd service file is included at deploy/dispatch.service. Copy it to /etc/systemd/system/, update the paths, and:

sudo systemctl enable dispatch
sudo systemctl start dispatch

The server reads .env from the working directory and auto-restarts on failure.

Tools

ToolDescription
web_searchFan-out search across Ollama, Brave, Jina, and DDG in parallel; results merged and deduplicated
web_fetchFetch full page content from a URL; falls back to Jina Reader for JS-rendered or thin pages

Files

main.py # CLI entry point and dispatcher
pipeline.py # AgentPipeline and MultiAgentPipeline orchestrators
agent_loader.py # loads DB agent rows → DomainConfig objects at runtime
agent_config.py # DomainConfig and SubQueryConfig dataclasses
agents/ # optional YAML files — import via UI or POST /api/agents/import
server.py # FastAPI HTTP server and web UI
db.py # SQLite schema, CRUD, and YAML seed loader
tools.py # Tool definitions (web_search, web_fetch) and dispatch
ui.html # single-page web UI (served at /)
logs.py # CLI tool for reviewing session history
requirements.txt
.env.example
deploy/ # systemd service file
search_history.db # created on first run (gitignore this)

SQLite schema

sessions (id, model, agent, source, started_at, ended_at, viewed_at, total_duration_ms)
messages (id, session_id, role, content, created_at)
tool_calls (id, session_id, tool_name, args, result, duration_ms, created_at)
dispatches (id, agent, prompt, schedule, label, enabled, run_on_create, created_at,
trigger_type, repeat_until, max_runs, run_count)
agents (id, name, label, description, enabled, color,
goal_template, output_template,
sources, sub_queries, tool_bindings, direct_sources,
freshness, planner_query_count, max_validator_loops,
validate_links, fetch_content,
is_builtin, created_at, updated_at)
settings (key TEXTPRIMARY KEY, value TEXT, updated_at TEXT)

source in sessions is cli, http, or cron. role in messages is user, assistant, or error.

dispatches.schedule is a standard 5-field cron expression. Schedules are managed by APScheduler inside the server process and persist across restarts.

agents.color is a hue integer (0–359). Auto-assigned randomly when an agent is first created if not specified in YAML.

Querying history

CLI (logs.py)

python logs.py # last 50 sessions
python logs.py -n 100 # last 100 sessions
python logs.py --agent news # filter by agent
python logs.py --session 42 # full detail for session 42

Direct SQLite

-- Recent sessions with first promptSELECTs.id, s.agent, s.started_at,
(SELECT content FROM messages WHERE session_id=s.idAND role='user'ORDER BY id LIMIT1) AS prompt
FROM sessions s ORDER BYs.idDESCLIMIT20;
-- Search by keywordSELECT*FROM messages WHERE content LIKE'%keyword%';
-- Tool calls for a sessionSELECT tool_name, args, result FROM tool_calls WHERE session_id=42;

Python (db.py)

importdbsessions=db.get_sessions(limit=50, agent="news")
detail=db.get_session_detail(42)
results=db.search_sessions(agent="news", keyword="market")

HTTP API

GET / web UI
GET /health sanity check
POST /api/jobs submit a job (async)
GET /api/jobs/{job_id} poll status / result
DELETE /api/jobs/{job_id} cancel a running job
GET /api/sessions list sessions (?agent=news&limit=50)
GET /api/sessions/{id} full session detail (messages + tool calls with duration)
GET /api/dispatches list dispatches
POST /api/dispatches create a dispatch
PUT /api/dispatches/{id} update a dispatch
DELETE /api/dispatches/{id} delete a dispatch
GET /api/agents list all agents
GET /api/agents/{name} full agent detail
POST /api/agents create an agent
PUT /api/agents/{name} update an agent
DELETE /api/agents/{name} delete a non-builtin agent
GET /api/agents/{name}/export export agent as YAML
POST /api/agents/import create/update agent from YAML body
GET /api/tools list available tools
GET /api/settings current settings (values from live environment)
POST /api/settings save settings to DB + .env (restart required to apply)
POST /v1/chat/completions OpenAI-compatible (synchronous)
GET /v1/models

Authentication

python -c "import secrets; print(secrets.token_urlsafe(32))"

Set as SERVER_API_KEY=... in .env. If unset, all requests are accepted.

Note: When SERVER_API_KEY is set, the server injects it into the web UI's HTML on page load so the frontend can authenticate its API calls automatically. This means the key is visible in the browser's View Source on the / route. Keep the server on a trusted network (localhost, Tailscale, private LAN) — don't bind to a public interface without additional network-level access controls.

License

MIT — see LICENSE.

About

Self-hosted AI research agent platform. Define agents, schedule them as dispatches, and get synthesized digests — no manual searching. Bring your own LLM (Ollama, OpenAI, Anthropic, Fireworks).

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

dispatch

License: MIT

Self-hosted AI research agent platform. Define agents for topics you care about, schedule them as dispatches, and get synthesized digests — no manual searching. Runs on your machine or server. Bring your own LLM (Ollama, OpenAI, Anthropic, Fireworks).

Read Digest

What you get

  • Web UI to browse, schedule, and read agent output — no config files required
  • Daily digest compiled from the most recent run of each active agent
  • Multi-track pipelines that fan out searches across independent topic tracks in parallel

Quick Start

# 1. Create and activate a virtual environment
python -m venv dispatch-env
source dispatch-env/bin/activate
# 2. Install dependencies
pip install -r requirements.txt
# 3. Configure environment
cp .env.example .env
# Set SERVER_API_KEY if the server will be network-accessible (see .env.example)# Everything else — provider, model, API keys, location — can be set in the Settings UI# 4. Pull a model (Ollama only — skip if using a cloud provider)
ollama pull qwen3
# 5. Start the server
python main.py --serve # http://localhost:8000

Open the web UI, go to Library, create your first agent, then click Schedule to activate it.

How agents work

There are two distinct concepts: agents and dispatches.

  • Agent — what an agent is: its name, goal, sources, search settings, and color. Agents live in the database and are the source of truth for behavior.
  • Dispatch — an agent paired with a user prompt and a schedule. One agent can have multiple dispatches (e.g., the news agent running daily at 7am with a general prompt and again on Fridays with a weekly-recap prompt).

No agents are active by default. Create agents in the Library tab, then activate them by creating a dispatch.

Library tab — create and browse agents
↓
Schedule button → creates a dispatch (agent + prompt + schedule)
↓
Active tab — running scheduled dispatches

How it works

Each agent run uses a multi-phase micro-agent pipeline. Each phase is a small, focused LLM call with a constrained tool budget. The orchestrator (Python) manages state between phases; no single model call holds the full context.

query
→ Planner (LLM) — generate N search queries (or skip if direct_sources set)
→ Searcher (API) — fire all queries in parallel, collect results
→ Validator (LLM) — coverage sufficient? retry with new queries if not
→ Synthesizer (LLM) — format final answer from research buffer

Optional phases (enabled per-agent via config):

  • Link validator — HEAD-checks result URLs, drops dead ones before synthesis
  • Ranker + Fetcher — LLM selects top URLs → parallel web_fetch → per-page summarizer

Adding a new agent

Option 1 — Web UI

Configure Agent

Library tab → New agent → fill in name, goal, sources, and color → Save. The agent immediately appears in the Library and can be scheduled.

Option 2 — YAML

Drop .yaml files into your agents directory (defaults to ./agents, override with DISPATCH_AGENTS_DIR in .env). No agents folder is included in the repo — create it and add your own files.

Create agents/my-agent.yaml:

name: my-agentlabel: My Agentdescription: What this agent covers.color: 175# hue 0–359; omit to auto-assign on first seedgoal_template: "Find recent {query}. Focus on X, Y, Z."output_template: "Return a bulleted digest with source links."sources:
- example.com
- anothersource.orgtool_bindings:
- web_searchplanner_query_count: 5freshness: w # d=24h w=7d m=30d y=1y (omit = any date)

Restart the server — the agent appears in the Library tab. Click Schedule to activate it.

Option 3 — Import YAML

Library tab → Import or POST /api/agents/import with a YAML body. Useful for migrating agents between instances.

YAML field reference

FieldTypeDefaultDescription
namestringrequiredUnique slug — used as the agent identifier everywhere
labelstring""Display name shown in pills and headers
descriptionstring""Short summary shown in the Library table
colorintegerrandomHue (0–359) for the color pill. Auto-assigned on first seed if omitted; never overwritten by re-seeding
goal_templatestringrequiredLLM prompt for what to find. {query} → user input, {today} → YYYY-MM-DD
output_templatestring""Instructions for how the synthesizer should format the response
sourceslist[]Domains to bias planner queries toward (e.g. sfchronicle.com)
direct_sourceslist[]URLs fetched directly, bypassing the planner. {today} substitution supported
sub_querieslistnullMulti-track pipelines — each track runs an isolated search pass (see Pipeline modes)
tool_bindingslist["web_search"]Tools available to this agent. Empty list = all tools
freshnessstringnullDate filter: dwmy
planner_query_countinteger5Number of search queries the planner generates
max_validator_loopsinteger1How many times the validator can request additional searches
validate_linksboolfalseHEAD-check each result URL and drop dead ones before synthesis
fetch_contentboolfalseRank URLs by relevance, fetch top 5 full pages, summarize per page

Pipeline modes

The pipeline is selected automatically based on your config. All modes end with a Validator loop and a Synthesizer call.

1. Search (default)

# no direct_sourcestool_bindings: [web_search]

LLM planner generates planner_query_count queries → parallel web search across all providers → validate → synthesize.

Best for news, research, any topic where broad query coverage helps. sources narrows the planner's focus toward specific domains without locking it to them.

2. Search + deep read

tool_bindings: [web_search, web_fetch]fetch_content: true

Same as search, but after collecting results the pipeline ranks all URLs by relevance, fetches the top 5 full pages, runs a per-page summarizer, and uses the extracted facts for synthesis instead of raw snippets.

Best for agents where the article body matters — detailed reporting, analysis, documentation. Slower (~2× more LLM calls) but higher-quality output.

3. Direct feed

direct_sources:
- https://example.com/feed?date={today}
- https://api.example.com/poststool_bindings: [web_fetch] # required — or omit tool_bindings entirely

Skips the LLM planner entirely. Each URL is fetched directly (with {today} substituted as YYYY-MM-DD). Results go straight into the research buffer.

Best for RSS feeds, known API endpoints, or structured data pages updated on a schedule. Note: tool_bindings must include web_fetch or be empty — the default ["web_search"] will silently skip direct URLs.

4. Domain-biased search

sources:
- sfchronicle.com
- missionlocal.org

The LLM planner is given sources as a hint — it generates queries biased toward those domains but still searches the open web. Not a hard filter.

Best for agents that should prefer specific publications without being locked to them.

5. Multi-track

sub_queries:
- label: researchgoal: "Find recent ML papers about {query}"sources: [arxiv.org, paperswithcode.com]query_count: 3freshness: w
- label: industrygoal: "Find business news about {query} in AI"sources: [techcrunch.com, venturebeat.com]query_count: 3

N independent search pipelines run in parallel, each with its own goal, sources, freshness, and query count. Results are aggregated into a final synthesizer call.

Best for agents covering multiple distinct topics that benefit from isolated query sets (e.g., AI papers vs industry news vs tooling). Note: fetch_content and direct_sources are not inherited by sub-pipelines — each track is search-only.

Options that apply across all modes

validate_links — HEAD-checks every result URL and drops dead ones before synthesis. Adds a few seconds, improves source quality.

max_validator_loops — the validator judges whether the research buffer is sufficient. If not, it generates retry queries and reruns the search phase. Default 1 means one pass (no retry). Set to 23 for agents where completeness matters more than speed.

LLM providers

Configure via the Settings UI (gear icon) or set in .env. Cloud providers automatically use larger context windows and higher parallelism than local Ollama.

ProviderPROVIDER=Recommended models
Ollama (default)ollamaqwen3 / qwen3:1.7b (fast)
OpenAIopenaigpt-4.1 / gpt-4.1-mini (fast)
Anthropicanthropicclaude-sonnet-4-6 / claude-haiku-4-5-20251001 (fast)
Fireworksfireworksllama-v3p3-70b-instruct / llama-v3p1-8b-instruct (fast)

Set MODEL for the primary model and optionally FAST_MODEL for a smaller model used in the Planner/Validator phases.

Search providers

Web search fans out across multiple providers in parallel, merges and deduplicates results. DuckDuckGo works out of the box — no key required. Add any of the following free-tier providers to expand coverage:

ProviderKeyFree tierNotes
Ollama CloudOLLAMA_API_KEYollama.comRecommended; primary fan-out provider
Brave SearchBRAVE_API_KEY2K queries/month — brave.com/search/apiGood general coverage
Jina SearchJINA_API_KEYjina.aiAlso enables Jina Reader for page fetching; site: queries excluded
DuckDuckGononealways-onBaseline; 60s circuit-breaker, auto-recovers

Page fetching uses requests with automatic fallback to Jina Reader (r.jina.ai) when a page returns thin content — catches JavaScript-rendered shells transparently. Set WEB_FETCH_PROVIDER=jina to always use Jina.

Environment variables

Required (set in .env before first run)

These are read at startup before the UI is accessible and cannot be changed via the Settings UI.

VariableDefaultDescription
SERVER_API_KEYunsetBearer token for the HTTP API. Set this if the server is network-accessible — without it anyone who can reach the host can trigger runs and read your data. Not needed for localhost use.
SERVER_HOST0.0.0.0Interface to bind
SERVER_PORT8000Port to listen on

Configurable in the Settings UI

Open the gear icon in the web UI to change these at any time. A server restart is required for changes to take effect. See .env.example for the full list with comments.

VariableDefaultDescription
PROVIDERollamaLLM backend: ollama, openai, anthropic, fireworks
MODELqwen3Primary model for synthesis
FAST_MODELunsetSmaller model for Planner/Validator phases; falls back to MODEL
OLLAMA_BASE_URLhttp://localhost:11434Ollama server address
OPENAI_API_KEYRequired if PROVIDER=openai
ANTHROPIC_API_KEYRequired if PROVIDER=anthropic
FIREWORKS_API_KEYRequired if PROVIDER=fireworks
BRAVE_API_KEYBrave Search — 2K queries/month free
JINA_API_KEYJina Search + higher Reader rate limits
LOCATIONunsetCity/region injected into planner prompt for location-aware searches
CONTEXT_BUDGETcloudcloud / local / minimal — controls content truncation for local models
TEMPERATURE0.2LLM sampling temperature
TIMEOUT300LLM request timeout in seconds

Not in Settings UI (.env only)

VariableDefaultDescription
OLLAMA_API_KEYOllama Cloud search key — ollama.com
WEB_FETCH_PROVIDERhttphttp (raw fetch + Jina fallback) or jina (always use Jina Reader)
DISPATCH_DB_PATHsearch_history.dbSQLite path
DISPATCH_AGENTS_DIR./agentsYAML seed files directory
PLANNER_QUERY_COUNT5Global default for planner_query_count (overridden per-agent)
PARALLEL_SUB_PIPELINES1Max parallel workers for multi-track pipelines; 0 = sequential

Web UI

The web UI (/) is a single-page app. The main area shows the current conversation; the sidebar manages agents and schedules.

Tabs

Digest — a compiled briefing assembled from the most recent run of each active agent. Skips agents that haven't run yet.

Active — all enabled dispatches (agent + prompt + schedule). Each card shows the agent pill, label, next run time, and last run status. Click a card to view the last output, edit the schedule, or run it immediately.

Archived — disabled dispatches. Re-enable from here without losing the schedule.

Library — all agents in the database, whether scheduled or not. Browse, create, edit, and export agents here.

Agent type

When creating or editing an agent, select the agent type first:

  • Web Search — the planner generates queries and searches the web. Configure preferred domains, fetch content, and freshness filter.
  • Direct Fetch — provide a list of URLs to fetch directly (no search). {today} is substituted with the current date. Best for feeds and known API endpoints.

Tool bindings are inferred automatically from the type and don't need to be set manually.

Creating and scheduling an agent

  1. Library → New agent — give it a name, label, goal template, sources, and optionally a color swatch
  2. Save — the agent appears in the Library table immediately
  3. Click Schedule on the new row — the dispatch sheet opens with the agent pre-selected
  4. Enter the prompt the agent will run with (e.g. "SF local news digest"), set frequency and time, save
  5. The agent appears in Active and will run on schedule

Schedule Dispatch

Editing agents

Accessible from Library → Edit or by clicking a row. Fields map directly to the YAML schema above. Changes take effect on the next run without a server restart.

Settings

The gear icon (top-right) opens the Settings sheet. Changes are saved to .env and take effect after a server restart.

SectionWhat you can configure
LLM ProviderProvider, model, API key, Ollama base URL, fast model
SearchOllama Cloud, Brave, and Jina API keys
PersonalizationLocation (used by local-aware agents)
AdvancedContext budget, temperature, LLM timeout

Other UI features

  • + New — start a one-off prompt against any agent (no schedule)
  • think — toggle Qwen3 reasoning mode for the current prompt
  • Stop — cancel a running job; shows live elapsed time while running
  • Responses rendered as markdown with clickable links

CLI

python main.py --serve # start server
python main.py --agent news # interactive
python main.py --agent news --once # run once and exit
python main.py --agent apartment --once --query "2br sf soma under 3000"

Scheduling via system crontab (alternative to UI schedules)

0 7 ***cd /path/to/dispatch && python main.py --agent my-agent --once
0 8 ** 1 cd /path/to/dispatch && python main.py --agent my-agent --once --query "weekly recap"

Deploying as a service

A systemd service file is included at deploy/dispatch.service. Copy it to /etc/systemd/system/, update the paths, and:

sudo systemctl enable dispatch
sudo systemctl start dispatch

The server reads .env from the working directory and auto-restarts on failure.

Tools

ToolDescription
web_searchFan-out search across Ollama, Brave, Jina, and DDG in parallel; results merged and deduplicated
web_fetchFetch full page content from a URL; falls back to Jina Reader for JS-rendered or thin pages

Files

main.py # CLI entry point and dispatcher
pipeline.py # AgentPipeline and MultiAgentPipeline orchestrators
agent_loader.py # loads DB agent rows → DomainConfig objects at runtime
agent_config.py # DomainConfig and SubQueryConfig dataclasses
agents/ # optional YAML files — import via UI or POST /api/agents/import
server.py # FastAPI HTTP server and web UI
db.py # SQLite schema, CRUD, and YAML seed loader
tools.py # Tool definitions (web_search, web_fetch) and dispatch
ui.html # single-page web UI (served at /)
logs.py # CLI tool for reviewing session history
requirements.txt
.env.example
deploy/ # systemd service file
search_history.db # created on first run (gitignore this)

SQLite schema

sessions (id, model, agent, source, started_at, ended_at, viewed_at, total_duration_ms)
messages (id, session_id, role, content, created_at)
tool_calls (id, session_id, tool_name, args, result, duration_ms, created_at)
dispatches (id, agent, prompt, schedule, label, enabled, run_on_create, created_at,
trigger_type, repeat_until, max_runs, run_count)
agents (id, name, label, description, enabled, color,
goal_template, output_template,
sources, sub_queries, tool_bindings, direct_sources,
freshness, planner_query_count, max_validator_loops,
validate_links, fetch_content,
is_builtin, created_at, updated_at)
settings (key TEXTPRIMARY KEY, value TEXT, updated_at TEXT)

source in sessions is cli, http, or cron. role in messages is user, assistant, or error.

dispatches.schedule is a standard 5-field cron expression. Schedules are managed by APScheduler inside the server process and persist across restarts.

agents.color is a hue integer (0–359). Auto-assigned randomly when an agent is first created if not specified in YAML.

Querying history

CLI (logs.py)

python logs.py # last 50 sessions
python logs.py -n 100 # last 100 sessions
python logs.py --agent news # filter by agent
python logs.py --session 42 # full detail for session 42

Direct SQLite

-- Recent sessions with first promptSELECTs.id, s.agent, s.started_at,
(SELECT content FROM messages WHERE session_id=s.idAND role='user'ORDER BY id LIMIT1) AS prompt
FROM sessions s ORDER BYs.idDESCLIMIT20;
-- Search by keywordSELECT*FROM messages WHERE content LIKE'%keyword%';
-- Tool calls for a sessionSELECT tool_name, args, result FROM tool_calls WHERE session_id=42;

Python (db.py)

importdbsessions=db.get_sessions(limit=50, agent="news")
detail=db.get_session_detail(42)
results=db.search_sessions(agent="news", keyword="market")

HTTP API

GET / web UI
GET /health sanity check
POST /api/jobs submit a job (async)
GET /api/jobs/{job_id} poll status / result
DELETE /api/jobs/{job_id} cancel a running job
GET /api/sessions list sessions (?agent=news&limit=50)
GET /api/sessions/{id} full session detail (messages + tool calls with duration)
GET /api/dispatches list dispatches
POST /api/dispatches create a dispatch
PUT /api/dispatches/{id} update a dispatch
DELETE /api/dispatches/{id} delete a dispatch
GET /api/agents list all agents
GET /api/agents/{name} full agent detail
POST /api/agents create an agent
PUT /api/agents/{name} update an agent
DELETE /api/agents/{name} delete a non-builtin agent
GET /api/agents/{name}/export export agent as YAML
POST /api/agents/import create/update agent from YAML body
GET /api/tools list available tools
GET /api/settings current settings (values from live environment)
POST /api/settings save settings to DB + .env (restart required to apply)
POST /v1/chat/completions OpenAI-compatible (synchronous)
GET /v1/models

Authentication

python -c "import secrets; print(secrets.token_urlsafe(32))"

Set as SERVER_API_KEY=... in .env. If unset, all requests are accepted.

Note: When SERVER_API_KEY is set, the server injects it into the web UI's HTML on page load so the frontend can authenticate its API calls automatically. This means the key is visible in the browser's View Source on the / route. Keep the server on a trusted network (localhost, Tailscale, private LAN) — don't bind to a public interface without additional network-level access controls.

License

MIT — see LICENSE.

About

Self-hosted AI research agent platform. Define agents, schedule them as dispatches, and get synthesized digests — no manual searching. Bring your own LLM (Ollama, OpenAI, Anthropic, Fireworks).

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

dispatch

License: MIT

Self-hosted AI research agent platform. Define agents for topics you care about, schedule them as dispatches, and get synthesized digests — no manual searching. Runs on your machine or server. Bring your own LLM (Ollama, OpenAI, Anthropic, Fireworks).

Read Digest

What you get

  • Web UI to browse, schedule, and read agent output — no config files required
  • Daily digest compiled from the most recent run of each active agent
  • Multi-track pipelines that fan out searches across independent topic tracks in parallel

Quick Start

# 1. Create and activate a virtual environment
python -m venv dispatch-env
source dispatch-env/bin/activate
# 2. Install dependencies
pip install -r requirements.txt
# 3. Configure environment
cp .env.example .env
# Set SERVER_API_KEY if the server will be network-accessible (see .env.example)# Everything else — provider, model, API keys, location — can be set in the Settings UI# 4. Pull a model (Ollama only — skip if using a cloud provider)
ollama pull qwen3
# 5. Start the server
python main.py --serve # http://localhost:8000

Open the web UI, go to Library, create your first agent, then click Schedule to activate it.

How agents work

There are two distinct concepts: agents and dispatches.

  • Agent — what an agent is: its name, goal, sources, search settings, and color. Agents live in the database and are the source of truth for behavior.
  • Dispatch — an agent paired with a user prompt and a schedule. One agent can have multiple dispatches (e.g., the news agent running daily at 7am with a general prompt and again on Fridays with a weekly-recap prompt).

No agents are active by default. Create agents in the Library tab, then activate them by creating a dispatch.

Library tab — create and browse agents
↓
Schedule button → creates a dispatch (agent + prompt + schedule)
↓
Active tab — running scheduled dispatches

How it works

Each agent run uses a multi-phase micro-agent pipeline. Each phase is a small, focused LLM call with a constrained tool budget. The orchestrator (Python) manages state between phases; no single model call holds the full context.

query
→ Planner (LLM) — generate N search queries (or skip if direct_sources set)
→ Searcher (API) — fire all queries in parallel, collect results
→ Validator (LLM) — coverage sufficient? retry with new queries if not
→ Synthesizer (LLM) — format final answer from research buffer

Optional phases (enabled per-agent via config):

  • Link validator — HEAD-checks result URLs, drops dead ones before synthesis
  • Ranker + Fetcher — LLM selects top URLs → parallel web_fetch → per-page summarizer

Adding a new agent

Option 1 — Web UI

Configure Agent

Library tab → New agent → fill in name, goal, sources, and color → Save. The agent immediately appears in the Library and can be scheduled.

Option 2 — YAML

Drop .yaml files into your agents directory (defaults to ./agents, override with DISPATCH_AGENTS_DIR in .env). No agents folder is included in the repo — create it and add your own files.

Create agents/my-agent.yaml:

name: my-agentlabel: My Agentdescription: What this agent covers.color: 175# hue 0–359; omit to auto-assign on first seedgoal_template: "Find recent {query}. Focus on X, Y, Z."output_template: "Return a bulleted digest with source links."sources:
- example.com
- anothersource.orgtool_bindings:
- web_searchplanner_query_count: 5freshness: w # d=24h w=7d m=30d y=1y (omit = any date)

Restart the server — the agent appears in the Library tab. Click Schedule to activate it.

Option 3 — Import YAML

Library tab → Import or POST /api/agents/import with a YAML body. Useful for migrating agents between instances.

YAML field reference

FieldTypeDefaultDescription
namestringrequiredUnique slug — used as the agent identifier everywhere
labelstring""Display name shown in pills and headers
descriptionstring""Short summary shown in the Library table
colorintegerrandomHue (0–359) for the color pill. Auto-assigned on first seed if omitted; never overwritten by re-seeding
goal_templatestringrequiredLLM prompt for what to find. {query} → user input, {today} → YYYY-MM-DD
output_templatestring""Instructions for how the synthesizer should format the response
sourceslist[]Domains to bias planner queries toward (e.g. sfchronicle.com)
direct_sourceslist[]URLs fetched directly, bypassing the planner. {today} substitution supported
sub_querieslistnullMulti-track pipelines — each track runs an isolated search pass (see Pipeline modes)
tool_bindingslist["web_search"]Tools available to this agent. Empty list = all tools
freshnessstringnullDate filter: dwmy
planner_query_countinteger5Number of search queries the planner generates
max_validator_loopsinteger1How many times the validator can request additional searches
validate_linksboolfalseHEAD-check each result URL and drop dead ones before synthesis
fetch_contentboolfalseRank URLs by relevance, fetch top 5 full pages, summarize per page

Pipeline modes

The pipeline is selected automatically based on your config. All modes end with a Validator loop and a Synthesizer call.

1. Search (default)

# no direct_sourcestool_bindings: [web_search]

LLM planner generates planner_query_count queries → parallel web search across all providers → validate → synthesize.

Best for news, research, any topic where broad query coverage helps. sources narrows the planner's focus toward specific domains without locking it to them.

2. Search + deep read

tool_bindings: [web_search, web_fetch]fetch_content: true

Same as search, but after collecting results the pipeline ranks all URLs by relevance, fetches the top 5 full pages, runs a per-page summarizer, and uses the extracted facts for synthesis instead of raw snippets.

Best for agents where the article body matters — detailed reporting, analysis, documentation. Slower (~2× more LLM calls) but higher-quality output.

3. Direct feed

direct_sources:
- https://example.com/feed?date={today}
- https://api.example.com/poststool_bindings: [web_fetch] # required — or omit tool_bindings entirely

Skips the LLM planner entirely. Each URL is fetched directly (with {today} substituted as YYYY-MM-DD). Results go straight into the research buffer.

Best for RSS feeds, known API endpoints, or structured data pages updated on a schedule. Note: tool_bindings must include web_fetch or be empty — the default ["web_search"] will silently skip direct URLs.

4. Domain-biased search

sources:
- sfchronicle.com
- missionlocal.org

The LLM planner is given sources as a hint — it generates queries biased toward those domains but still searches the open web. Not a hard filter.

Best for agents that should prefer specific publications without being locked to them.

5. Multi-track

sub_queries:
- label: researchgoal: "Find recent ML papers about {query}"sources: [arxiv.org, paperswithcode.com]query_count: 3freshness: w
- label: industrygoal: "Find business news about {query} in AI"sources: [techcrunch.com, venturebeat.com]query_count: 3

N independent search pipelines run in parallel, each with its own goal, sources, freshness, and query count. Results are aggregated into a final synthesizer call.

Best for agents covering multiple distinct topics that benefit from isolated query sets (e.g., AI papers vs industry news vs tooling). Note: fetch_content and direct_sources are not inherited by sub-pipelines — each track is search-only.

Options that apply across all modes

validate_links — HEAD-checks every result URL and drops dead ones before synthesis. Adds a few seconds, improves source quality.

max_validator_loops — the validator judges whether the research buffer is sufficient. If not, it generates retry queries and reruns the search phase. Default 1 means one pass (no retry). Set to 23 for agents where completeness matters more than speed.

LLM providers

Configure via the Settings UI (gear icon) or set in .env. Cloud providers automatically use larger context windows and higher parallelism than local Ollama.

ProviderPROVIDER=Recommended models
Ollama (default)ollamaqwen3 / qwen3:1.7b (fast)
OpenAIopenaigpt-4.1 / gpt-4.1-mini (fast)
Anthropicanthropicclaude-sonnet-4-6 / claude-haiku-4-5-20251001 (fast)
Fireworksfireworksllama-v3p3-70b-instruct / llama-v3p1-8b-instruct (fast)

Set MODEL for the primary model and optionally FAST_MODEL for a smaller model used in the Planner/Validator phases.

Search providers

Web search fans out across multiple providers in parallel, merges and deduplicates results. DuckDuckGo works out of the box — no key required. Add any of the following free-tier providers to expand coverage:

ProviderKeyFree tierNotes
Ollama CloudOLLAMA_API_KEYollama.comRecommended; primary fan-out provider
Brave SearchBRAVE_API_KEY2K queries/month — brave.com/search/apiGood general coverage
Jina SearchJINA_API_KEYjina.aiAlso enables Jina Reader for page fetching; site: queries excluded
DuckDuckGononealways-onBaseline; 60s circuit-breaker, auto-recovers

Page fetching uses requests with automatic fallback to Jina Reader (r.jina.ai) when a page returns thin content — catches JavaScript-rendered shells transparently. Set WEB_FETCH_PROVIDER=jina to always use Jina.

Environment variables

Required (set in .env before first run)

These are read at startup before the UI is accessible and cannot be changed via the Settings UI.

VariableDefaultDescription
SERVER_API_KEYunsetBearer token for the HTTP API. Set this if the server is network-accessible — without it anyone who can reach the host can trigger runs and read your data. Not needed for localhost use.
SERVER_HOST0.0.0.0Interface to bind
SERVER_PORT8000Port to listen on

Configurable in the Settings UI

Open the gear icon in the web UI to change these at any time. A server restart is required for changes to take effect. See .env.example for the full list with comments.

VariableDefaultDescription
PROVIDERollamaLLM backend: ollama, openai, anthropic, fireworks
MODELqwen3Primary model for synthesis
FAST_MODELunsetSmaller model for Planner/Validator phases; falls back to MODEL
OLLAMA_BASE_URLhttp://localhost:11434Ollama server address
OPENAI_API_KEYRequired if PROVIDER=openai
ANTHROPIC_API_KEYRequired if PROVIDER=anthropic
FIREWORKS_API_KEYRequired if PROVIDER=fireworks
BRAVE_API_KEYBrave Search — 2K queries/month free
JINA_API_KEYJina Search + higher Reader rate limits
LOCATIONunsetCity/region injected into planner prompt for location-aware searches
CONTEXT_BUDGETcloudcloud / local / minimal — controls content truncation for local models
TEMPERATURE0.2LLM sampling temperature
TIMEOUT300LLM request timeout in seconds

Not in Settings UI (.env only)

VariableDefaultDescription
OLLAMA_API_KEYOllama Cloud search key — ollama.com
WEB_FETCH_PROVIDERhttphttp (raw fetch + Jina fallback) or jina (always use Jina Reader)
DISPATCH_DB_PATHsearch_history.dbSQLite path
DISPATCH_AGENTS_DIR./agentsYAML seed files directory
PLANNER_QUERY_COUNT5Global default for planner_query_count (overridden per-agent)
PARALLEL_SUB_PIPELINES1Max parallel workers for multi-track pipelines; 0 = sequential

Web UI

The web UI (/) is a single-page app. The main area shows the current conversation; the sidebar manages agents and schedules.

Tabs

Digest — a compiled briefing assembled from the most recent run of each active agent. Skips agents that haven't run yet.

Active — all enabled dispatches (agent + prompt + schedule). Each card shows the agent pill, label, next run time, and last run status. Click a card to view the last output, edit the schedule, or run it immediately.

Archived — disabled dispatches. Re-enable from here without losing the schedule.

Library — all agents in the database, whether scheduled or not. Browse, create, edit, and export agents here.

Agent type

When creating or editing an agent, select the agent type first:

  • Web Search — the planner generates queries and searches the web. Configure preferred domains, fetch content, and freshness filter.
  • Direct Fetch — provide a list of URLs to fetch directly (no search). {today} is substituted with the current date. Best for feeds and known API endpoints.

Tool bindings are inferred automatically from the type and don't need to be set manually.

Creating and scheduling an agent

  1. Library → New agent — give it a name, label, goal template, sources, and optionally a color swatch
  2. Save — the agent appears in the Library table immediately
  3. Click Schedule on the new row — the dispatch sheet opens with the agent pre-selected
  4. Enter the prompt the agent will run with (e.g. "SF local news digest"), set frequency and time, save
  5. The agent appears in Active and will run on schedule

Schedule Dispatch

Editing agents

Accessible from Library → Edit or by clicking a row. Fields map directly to the YAML schema above. Changes take effect on the next run without a server restart.

Settings

The gear icon (top-right) opens the Settings sheet. Changes are saved to .env and take effect after a server restart.

SectionWhat you can configure
LLM ProviderProvider, model, API key, Ollama base URL, fast model
SearchOllama Cloud, Brave, and Jina API keys
PersonalizationLocation (used by local-aware agents)
AdvancedContext budget, temperature, LLM timeout

Other UI features

  • + New — start a one-off prompt against any agent (no schedule)
  • think — toggle Qwen3 reasoning mode for the current prompt
  • Stop — cancel a running job; shows live elapsed time while running
  • Responses rendered as markdown with clickable links

CLI

python main.py --serve # start server
python main.py --agent news # interactive
python main.py --agent news --once # run once and exit
python main.py --agent apartment --once --query "2br sf soma under 3000"

Scheduling via system crontab (alternative to UI schedules)

0 7 ***cd /path/to/dispatch && python main.py --agent my-agent --once
0 8 ** 1 cd /path/to/dispatch && python main.py --agent my-agent --once --query "weekly recap"

Deploying as a service

A systemd service file is included at deploy/dispatch.service. Copy it to /etc/systemd/system/, update the paths, and:

sudo systemctl enable dispatch
sudo systemctl start dispatch

The server reads .env from the working directory and auto-restarts on failure.

Tools

ToolDescription
web_searchFan-out search across Ollama, Brave, Jina, and DDG in parallel; results merged and deduplicated
web_fetchFetch full page content from a URL; falls back to Jina Reader for JS-rendered or thin pages

Files

main.py # CLI entry point and dispatcher
pipeline.py # AgentPipeline and MultiAgentPipeline orchestrators
agent_loader.py # loads DB agent rows → DomainConfig objects at runtime
agent_config.py # DomainConfig and SubQueryConfig dataclasses
agents/ # optional YAML files — import via UI or POST /api/agents/import
server.py # FastAPI HTTP server and web UI
db.py # SQLite schema, CRUD, and YAML seed loader
tools.py # Tool definitions (web_search, web_fetch) and dispatch
ui.html # single-page web UI (served at /)
logs.py # CLI tool for reviewing session history
requirements.txt
.env.example
deploy/ # systemd service file
search_history.db # created on first run (gitignore this)

SQLite schema

sessions (id, model, agent, source, started_at, ended_at, viewed_at, total_duration_ms)
messages (id, session_id, role, content, created_at)
tool_calls (id, session_id, tool_name, args, result, duration_ms, created_at)
dispatches (id, agent, prompt, schedule, label, enabled, run_on_create, created_at,
trigger_type, repeat_until, max_runs, run_count)
agents (id, name, label, description, enabled, color,
goal_template, output_template,
sources, sub_queries, tool_bindings, direct_sources,
freshness, planner_query_count, max_validator_loops,
validate_links, fetch_content,
is_builtin, created_at, updated_at)
settings (key TEXTPRIMARY KEY, value TEXT, updated_at TEXT)

source in sessions is cli, http, or cron. role in messages is user, assistant, or error.

dispatches.schedule is a standard 5-field cron expression. Schedules are managed by APScheduler inside the server process and persist across restarts.

agents.color is a hue integer (0–359). Auto-assigned randomly when an agent is first created if not specified in YAML.

Querying history

CLI (logs.py)

python logs.py # last 50 sessions
python logs.py -n 100 # last 100 sessions
python logs.py --agent news # filter by agent
python logs.py --session 42 # full detail for session 42

Direct SQLite

-- Recent sessions with first promptSELECTs.id, s.agent, s.started_at,
(SELECT content FROM messages WHERE session_id=s.idAND role='user'ORDER BY id LIMIT1) AS prompt
FROM sessions s ORDER BYs.idDESCLIMIT20;
-- Search by keywordSELECT*FROM messages WHERE content LIKE'%keyword%';
-- Tool calls for a sessionSELECT tool_name, args, result FROM tool_calls WHERE session_id=42;

Python (db.py)

importdbsessions=db.get_sessions(limit=50, agent="news")
detail=db.get_session_detail(42)
results=db.search_sessions(agent="news", keyword="market")

HTTP API

GET / web UI
GET /health sanity check
POST /api/jobs submit a job (async)
GET /api/jobs/{job_id} poll status / result
DELETE /api/jobs/{job_id} cancel a running job
GET /api/sessions list sessions (?agent=news&limit=50)
GET /api/sessions/{id} full session detail (messages + tool calls with duration)
GET /api/dispatches list dispatches
POST /api/dispatches create a dispatch
PUT /api/dispatches/{id} update a dispatch
DELETE /api/dispatches/{id} delete a dispatch
GET /api/agents list all agents
GET /api/agents/{name} full agent detail
POST /api/agents create an agent
PUT /api/agents/{name} update an agent
DELETE /api/agents/{name} delete a non-builtin agent
GET /api/agents/{name}/export export agent as YAML
POST /api/agents/import create/update agent from YAML body
GET /api/tools list available tools
GET /api/settings current settings (values from live environment)
POST /api/settings save settings to DB + .env (restart required to apply)
POST /v1/chat/completions OpenAI-compatible (synchronous)
GET /v1/models

Authentication

python -c "import secrets; print(secrets.token_urlsafe(32))"

Set as SERVER_API_KEY=... in .env. If unset, all requests are accepted.

Note: When SERVER_API_KEY is set, the server injects it into the web UI's HTML on page load so the frontend can authenticate its API calls automatically. This means the key is visible in the browser's View Source on the / route. Keep the server on a trusted network (localhost, Tailscale, private LAN) — don't bind to a public interface without additional network-level access controls.

License

MIT — see LICENSE.

About

Self-hosted AI research agent platform. Define agents, schedule them as dispatches, and get synthesized digests — no manual searching. Bring your own LLM (Ollama, OpenAI, Anthropic, Fireworks).

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages