Adds Perplexity Sonar, Claude web_search, OpenAI Responses, and Gemini
Grounding as ResearchAgents behind the same comparison interface as the
search and extract providers.
New endpoints:
POST /v1/research — single-agent (or auto-routed to the first
provider with a configured key)
POST /v1/research/compare — fan-out across N agents, persist all
answers + citations in research.eval_*
Each agent normalizes its native response into a common AgentAnswer shape
(answer text + citations[] + tokenUsage), storing the provider's raw
response alongside for later inspection. Implementations use direct HTTP
against each vendor's public API — no SDK deps added.
Auto-routing preference: perplexity-sonar → gemini-grounding →
openai-responses → claude-web-search → (openai-deep-research stubbed for
Phase 3b). Credits orchestration reuses the search/extract executor
pattern (reserve → call → commit/refund).
Deferred to Phase 3b: openai-deep-research (async job queue), migration
of mana-ai + mana-api news-research to call this service directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
28 KiB
Mana Research Service — Plan
Status (2026-04-17)
Planung, noch kein Code. Neuer Service services/mana-research (Bun/Hono, Port 3068), der 16+ Web-Research-Provider hinter einer einheitlichen Schnittstelle bündelt. Ziel: Vergleichbarkeit zwischen Anbietern + beste verfügbare Research-Qualität je nach Query-Typ.
Vorausgegangene Analyse: docs/reports/web-research-capabilities.md
Verwandtes Modul: docs/plans/news-research-module.md (das bestehende RSS-Modul, wird in Phase 3 auf den neuen Service migriert)
Ziel
Eine zentrale Research-Schicht, die:
- Viele Anbieter bündelt hinter einer gemeinsamen Schnittstelle (Search, Extract, Agent).
- Side-by-Side-Vergleich erlaubt (gleiche Query → N Provider parallel, normalisierte + rohe Ergebnisse in PostgreSQL).
- Auto-Routing nach Query-Typ macht (News → Tavily, Paper → Exa, komplexe Frage → Perplexity, …).
- Pay-per-use-only: keine Services mit Monats-Abo, nur API-Call-basierte Abrechnung.
- mana-credits integriert ist — jeder Call kostet je nach Provider + Operation.
- Hybrid-Keys unterstützt — Server-Keys als Default (charged via credits), BYO-Keys pro User optional (kein Credit-Verbrauch).
- Frontend Research Lab liefert — UI zum Eingeben von Queries, Auswahl von Providern, Side-by-Side-Review, manuelles Rating.
Abgrenzung
mana-search(Go, Port 3021) bleibt als SearXNG+Readability-Primitive bestehen und wird ein Provider im neuen Service. Keine Ablösung, nur Umfassung.mana-crawler(Go, Port 3023) bleibt als Deep-Crawl-Tool separat. Wird nicht Teil der Research-Pipeline.news-research-Modul im Frontend und die/api/v1/news-research/*-Routen in mana-api bleiben zunächst parallel bestehen und werden in Phase 3 als dünner Adapter auf den neuen Service umgestellt.mana-aibehält seinen eigenen NewsResearchClient, wird in Phase 3 aufmana-researchumgestellt.
1. Architektur
apps/mana web / apps/api / mana-ai
│
▼
mana-research (3068)
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
SearchProvider ExtractProvider ResearchAgent
│ │ │
┌──────────┼──────────┐ │ ┌──────────┼──────────┐
│ │ │ │ │ │ │
SearXNG Brave Tavily Readab Perplex Claude OpenAI
DDG Exa Serper Jina Sonar Web-Search Responses
Mojeek SerpAPI Firecr Gemini DR (async)
ScrapBee Ground
┌──────────────────────────────────┐
│ Redis Cache (query+provider) │
│ Postgres research.{eval_runs, │
│ eval_results, │
│ provider_configs, │
│ provider_stats} │
│ mana-credits (cost tracking) │
│ mana-llm (query classification) │
└──────────────────────────────────┘
Service-Grenze
- Eigener Hono-Server in
services/mana-research/src/server.ts. - Auth:
@mana/shared-honoauthMiddleware wie alle anderen Services. Interne Service-to-Service-Calls (mana-ai) nutzen weiterhin den vorhandenen Muster ohne User-Auth. - CORS-Origins:
*.mana.how+localhost:5173— wird in Phase 1 gesetzt. - Dependencies:
mana-llm(für Query-Klassifikation),mana-credits(für Cost-Tracking),mana-search(als Provider gewrappt), PostgreSQL (mana_platform), Redis (Cache).
Provider-Interfaces (TypeScript, exportiert aus packages/shared-research)
Neues Shared-Package @mana/shared-research für Typen, die vom Frontend (research-lab Modul) und Backend (mana-research + mana-api) gemeinsam genutzt werden.
// Gemeinsame Metadaten jedes Provider-Calls
export interface ProviderMeta {
provider: ProviderId; // 'brave' | 'tavily' | ...
category: 'search' | 'extract' | 'agent';
latencyMs: number;
costCredits: number; // via creditsPerCall mapping
cacheHit: boolean;
billingMode: 'server-key' | 'byo-key';
errorCode?: string; // wenn success=false
}
export interface SearchHit {
url: string;
title: string;
snippet: string;
publishedAt?: string;
author?: string;
score?: number; // provider-eigenes Ranking
content?: string; // wenn Provider direkt Text liefert (Tavily)
providerRaw: unknown; // roh für Debug
}
export interface ExtractedContent {
url: string;
title: string;
content: string; // Markdown oder plain text
excerpt?: string;
author?: string;
siteName?: string;
publishedAt?: string;
wordCount: number;
providerRaw: unknown;
}
export interface AgentAnswer {
query: string;
answer: string; // synthetisierte Antwort, Markdown
citations: Array<{ url: string; title: string; snippet?: string }>;
followUpQueries?: string[];
tokenUsage?: { input: number; output: number };
providerRaw: unknown;
}
export interface SearchProvider {
id: ProviderId;
capabilities: {
webSearch: boolean;
newsSearch: boolean;
scholarSearch: boolean;
semanticSearch: boolean;
contentInResults: boolean; // true wenn Tavily-style
};
search(query: string, opts: SearchOptions): Promise<{
results: SearchHit[];
meta: ProviderMeta;
}>;
}
export interface ExtractProvider {
id: ProviderId;
capabilities: {
jsRendering: boolean;
pdfSupport: boolean;
markdownOutput: boolean;
};
extract(url: string, opts: ExtractOptions): Promise<{
content: ExtractedContent;
meta: ProviderMeta;
}>;
}
export interface ResearchAgent {
id: ProviderId;
capabilities: {
multiStep: boolean;
async: boolean;
withCitations: boolean;
};
research(query: string, opts: AgentOptions): Promise<{
answer: AgentAnswer;
meta: ProviderMeta;
}>;
}
Service-Modi (HTTP-Endpoints)
| Endpoint | Zweck |
|---|---|
POST /v1/search |
Single-Provider-Search. Body: { query, provider?, options }. Wenn provider fehlt → Auto-Router. |
POST /v1/search/compare |
Fan-Out an N Provider, speichert Run in research.eval_runs. Body: { query, providers: [...], options }. |
POST /v1/extract |
Single-Provider-Extract. Body: { url, provider? }. |
POST /v1/extract/compare |
Fan-Out für Extract. |
POST /v1/research |
Agent-Mode. Query → synthesierte Antwort + Zitate. Body: { query, agent?, options }. |
POST /v1/research/compare |
Fan-Out an N Agents. |
GET /v1/providers |
Liste aller registrierten Provider + Capabilities + aktuelle Kosten. |
GET /v1/providers/health |
Welche Provider sind aktuell erreichbar / haben gültige Keys? |
GET /v1/runs |
User's gespeicherte Eval-Runs (paginiert). |
GET /v1/runs/:id |
Einzelner Run mit allen Results. |
POST /v1/runs/:id/results/:resultId/rate |
User-Rating für Eval. |
GET /health, /metrics |
Standard |
2. Provider-Inventar
Alle Provider nur mit Pay-per-use-Abrechnung, kein fester Monatsbetrag nötig.
2.1 Search Providers (6)
| Provider | Stärke | Kosten-Modell | Key nötig? | costCredits (geschätzt) |
|---|---|---|---|---|
searxng |
Self-hosted, unabhängig | Infrastruktur only | Nein | 0 |
brave |
Unabhängiger Index, Privacy | $5/1k Queries (PAYG) | Ja | 5 (≈ $0.005) |
tavily |
Agent-optimiert, liefert Content + Answer | Credit-Packs, kein Abo-Zwang ($30 = 4k credits, persistent) | Ja | 8 |
exa |
Semantische/Embedding-basierte Suche, Papers | $0.001-0.01/Query PAYG | Ja | 3–10 je nach Modus |
serper |
Google-SERP als JSON (inkl. People-Also-Ask, Knowledge Panel) | $0.30-1/1k (PAYG) | Ja | 1 |
duckduckgo |
Free Instant-Answer API | Kostenlos, rate-limited | Nein | 0 |
Optional später: mojeek (unabhängiger Index, PAYG), serpapi (Alternative zu Serper, höhere Coverage $5/1k).
2.2 Extract Providers (5)
| Provider | Stärke | Kosten-Modell | Key nötig? | costCredits |
|---|---|---|---|---|
readability |
Mozilla Readability via mana-search |
Kostenlos (self-hosted) | Nein | 0 |
jina-reader |
r.jina.ai, extrem robust, Markdown out |
Free-Tier 1M tokens/Monat, dann $0.02/1M tokens | Optional (höheres Rate-Limit mit Key) | 1 |
firecrawl |
JS-Rendering via Playwright, Batch-Crawls | PAYG Credits (oder self-host via Docker, dann 0) | Ja (bzw. intern für self-host) | 10 |
scrapingbee |
Proxy + JS-Render | PAYG Credits | Ja | 8 |
crawl4ai |
Self-hosted OSS (Python + Playwright) | Infrastruktur only | Nein | 0 |
2.3 Research Agents (5)
| Provider | Stärke | Kosten-Modell | Key nötig? | costCredits |
|---|---|---|---|---|
perplexity-sonar |
Beste Plug-and-Play-Research-API, 4 Modelle (sonar, sonar-pro, sonar-reasoning, sonar-deep-research) |
Token-based PAYG ($1-15/1M input + search fees) | Ja | 50–500 je nach Modell |
claude-web-search |
Claude-API mit server-seitigem web_search-Tool |
Token-based + $10/1k searches | Ja (Anthropic) | 100–300 |
openai-responses |
OpenAI Responses API mit web_search_preview-Tool |
Token-based + per-tool-call | Ja (OpenAI) | 100–300 |
gemini-grounding |
Gemini + Google Search Grounding | Token-based + per-grounding-query | Ja (Google) | 80–200 |
openai-deep-research |
Async, autonomer Multi-Step-Agent | Pay-per-task (~$0.10-1/task) | Ja | 1000+ (async via Job-Queue) |
Preise sind Stand 2026-04-17 und werden zentral in services/mana-research/src/providers/pricing.ts gepflegt und im Service-Startup aus einer JSON-Datei gezogen (updatebar ohne Redeploy).
3. Daten-Modell
Neues PostgreSQL-Schema research in mana_platform. Drizzle-Schema in services/mana-research/src/db/schema.ts.
export const researchSchema = pgSchema('research');
// Ein Run = eine Query, ein oder mehrere Provider
export const evalRuns = researchSchema.table('eval_runs', {
id: uuid('id').primaryKey().defaultRandom(),
userId: text('user_id'), // null für Service-to-Service
query: text('query').notNull(),
queryType: text('query_type'), // 'news' | 'general' | 'semantic' | 'academic' | 'code'
mode: text('mode').notNull(), // 'single' | 'compare' | 'auto'
category: text('category').notNull(),// 'search' | 'extract' | 'agent'
providersRequested: text('providers_requested').array().notNull(),
billingMode: text('billing_mode').notNull(), // 'server-key' | 'byo-key' | 'mixed'
totalCostCredits: integer('total_cost_credits').notNull().default(0),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
// Ein Result = Antwort von genau einem Provider
export const evalResults = researchSchema.table('eval_results', {
id: uuid('id').primaryKey().defaultRandom(),
runId: uuid('run_id').notNull().references(() => evalRuns.id, { onDelete: 'cascade' }),
providerId: text('provider_id').notNull(),
success: boolean('success').notNull(),
latencyMs: integer('latency_ms').notNull(),
costCredits: integer('cost_credits').notNull().default(0),
billingMode: text('billing_mode').notNull(),
cacheHit: boolean('cache_hit').notNull().default(false),
rawResponse: jsonb('raw_response'), // Provider-spezifisch, für Debug
normalizedResult: jsonb('normalized_result'),// SearchHit[] | ExtractedContent | AgentAnswer
errorCode: text('error_code'),
errorMessage: text('error_message'),
userRating: integer('user_rating'), // 1-5, null wenn nicht bewertet
userNotes: text('user_notes'),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
// Per-User Provider-Config (API-Keys, Limits)
export const providerConfigs = researchSchema.table('provider_configs', {
id: uuid('id').primaryKey().defaultRandom(),
userId: text('user_id'), // null = server-wide default
providerId: text('provider_id').notNull(),
apiKeyEncrypted: text('api_key_encrypted'),// verschlüsselt via shared-crypto (KEK-wrapped, AES-GCM)
enabled: boolean('enabled').notNull().default(true),
dailyBudgetCredits: integer('daily_budget_credits'),
monthlyBudgetCredits: integer('monthly_budget_credits'),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
}, (t) => ({
userProvider: uniqueIndex('user_provider_unique').on(t.userId, t.providerId),
}));
// Aggregierte Stats für Admin-Dashboard & Auto-Router
export const providerStats = researchSchema.table('provider_stats', {
providerId: text('provider_id').notNull(),
day: date('day').notNull(),
totalCalls: integer('total_calls').notNull().default(0),
totalLatencyMs: bigint('total_latency_ms', { mode: 'number' }).notNull().default(0),
totalCostCredits:integer('total_cost_credits').notNull().default(0),
successCount: integer('success_count').notNull().default(0),
errorCount: integer('error_count').notNull().default(0),
avgUserRating: real('avg_user_rating'),
ratingCount: integer('rating_count').notNull().default(0),
}, (t) => ({
pk: primaryKey({ columns: [t.providerId, t.day] }),
}));
Encryption: providerConfigs.apiKeyEncrypted wird via shared-crypto (existierendes AES-GCM + KEK-Pattern) verschlüsselt. Eintrag in apps/mana/apps/web/src/lib/data/crypto/registry.ts nötig, falls Client-Cache der Keys kommt — sonst serverseitig über mana-auth KEK.
4. Credits-Integration
Prinzip: Jeder Provider-Call hat festen costCredits-Preis (siehe §2). Bei Server-Key-Mode → Credits werden via mana-credits vom User abgezogen. Bei BYO-Key → 0 Credits, aber wir loggen den Call trotzdem (Usage-Transparenz).
Flow pro Call
// services/mana-research/src/middleware/credits.ts
async function chargeForCall(userId, providerId, operation) {
const config = await loadUserConfig(userId, providerId);
const isByoKey = !!config?.apiKeyEncrypted;
if (isByoKey) {
return { billingMode: 'byo-key', costCredits: 0, apiKey: decrypt(config.apiKeyEncrypted) };
}
// Server-Key-Mode: Credits prüfen + reservieren
const price = PROVIDER_PRICING[providerId][operation]; // aus pricing.ts
const balance = await manaCredits.getBalance(userId);
if (balance < price) throw new HTTPException(402, 'Insufficient credits');
// Soft-Reserve (atomic decrement)
await manaCredits.reserve(userId, price, `research:${providerId}:${operation}`);
return { billingMode: 'server-key', costCredits: price, apiKey: SERVER_KEYS[providerId] };
}
// Nach erfolgreichem Call: commit (bei Fehler: refund)
async function finalizeCharge(userId, reservationId, success) {
if (success) await manaCredits.commit(reservationId);
else await manaCredits.refund(reservationId);
}
mana-credits API (zu verifizieren / ggf. zu erweitern)
Erwartete Endpoints:
GET /api/v1/credits/balance?userId=...POST /api/v1/credits/reserve{ userId, amount, reason }→{ reservationId }POST /api/v1/credits/commit{ reservationId }POST /api/v1/credits/refund{ reservationId }GET /api/v1/credits/usage?userId=...&since=...&filter=research:*
Falls mana-credits diese Granularität noch nicht hat → Task in Phase 1 ergänzen: Reserve/Commit/Refund-Pattern einführen.
Budget-Enforcement
Pro User konfigurierbar in providerConfigs.dailyBudgetCredits / monthlyBudgetCredits. Default: unlimited (nur von mana-credits-Balance limitiert). UI in Settings → "Research Providers" (Phase 4).
5. BYO-Keys (Hybrid-Modus)
UX
Settings-Seite /settings/research-providers (Phase 4):
- Liste aller Provider mit Toggle "Server-Key nutzen" vs. "Eigener Key"
- Eingabefeld für API-Key (wird nicht angezeigt, nur
••••••) - Pro Provider optional tägliches/monatliches Budget setzen
Storage
- Keys verschlüsselt via bestehender
shared-crypto-Pipeline (AES-GCM-256, KEK ausmana-auth) - Tabelle
research.provider_configsmituserId+providerIdunique constraint - Beim Call: wenn User-Config existiert → verwende deren Key →
billingMode = 'byo-key'→ kein Credit-Verbrauch
Server-Keys
- Zentral in Env-Vars:
BRAVE_API_KEY,TAVILY_API_KEY,EXA_API_KEY,SERPER_API_KEY,PERPLEXITY_API_KEY,ANTHROPIC_API_KEY,OPENAI_API_KEY,GOOGLE_GENAI_API_KEY,FIRECRAWL_API_KEY,SCRAPINGBEE_API_KEY,JINA_API_KEY - Einheitlich via
services/mana-research/src/config.ts - In
.env.developmentdokumentiert +scripts/generate-env.mjsergänzt
6. Query-Klassifikation & Auto-Router
Für mode: 'auto': Query wird via mana-llm (gemma3:4b) klassifiziert → bester Provider je Typ.
// services/mana-research/src/router/classify.ts
export type QueryType = 'news' | 'general' | 'semantic' | 'academic' | 'code' | 'conversational';
const ROUTE_MAP: Record<QueryType, ProviderId[]> = {
news: ['tavily', 'brave', 'searxng'], // Tavily first (News-strong)
general: ['brave', 'tavily', 'serper'],
semantic: ['exa', 'tavily'], // Exa für "find similar to"
academic: ['exa', 'searxng'], // Exa findet Papers am besten
code: ['exa', 'serper', 'searxng'],
conversational: ['perplexity-sonar', 'claude-web-search'],// agent mode für Frage-Antwort
};
// Fallback-Chain: wenn Primary fehlschlägt, nächster in Liste
Klassifikation ist optional und fällt bei LLM-Timeout auf 'general' zurück.
7. Phasen-Plan
Phase 1 — Foundation + Core Providers ✅ (2026-04-17)
Ziel: Service läuft mit 4 Search-Providern + grundlegender Cache, Credits-Integration, /search + /search/compare Endpoints.
- Service-Scaffold
services/mana-research/(Bun/Hono + Drizzle +@mana/shared-hono) - Shared-Package
packages/shared-researchfür Provider-Typen - DB-Migration:
researchschema (4 Tabellen) - Provider-Adapter:
SearXNGProvider(wraptmana-search)BraveSearchProviderTavilyProviderDuckDuckGoProvider(als kostenloser Fallback)
POST /v1/search+POST /v1/search/compare- Redis-Cache (key:
research:${category}:${provider}:${sha256(query+opts)}, TTL 1h) - Credits-Middleware mit Reserve/Commit/Refund
pricing.ts+PROVIDER_PRICING-Map- Docker-Compose-Eintrag (
docker-compose.yml+docker-compose.macmini.yml) - Port-Eintrag in
docs/PORT_SCHEMA.md - Env-Vars:
BRAVE_API_KEY,TAVILY_API_KEYin.env.development services/mana-research/CLAUDE.md- Eintrag im Root-CLAUDE.md "Active services"
- Falls nötig: Erweiterung
mana-creditsum Reserve/Commit/Refund - Integration-Tests:
tests/search-providers.spec.ts(Mock-HTTP)
Phase 2 — Extraction + semantische Suche ✅ (2026-04-17)
- Provider-Adapter:
ReadabilityProvider(wraptmana-search /extract)JinaReaderProvider(zero-auth, optionaler Key für höheres Rate-Limit)FirecrawlProvider(PAYG + self-hostbar viaFIRECRAWL_API_URL)— deferred: liefert raw HTML, braucht zusätzlichen Readability-Pass. Wird als Phase-3-Addition behandelt.ScrapingBeeProviderExaProvider(semantische Suche in Phase 2 gelandet)SerperProvider(Google SERP als JSON)
POST /v1/extract+POST /v1/extract/compare- Query-Klassifikation:
classify.tsmit Regex-Fast-Path + optionalemmana-llm-Call (useLlmClassifier: true) - Auto-Router in
POST /v1/search(wennprovidernicht gesetzt) + Auto-Router für Extract - Provider-Health-Check-Endpoint
GET /v1/providers/health - Run-Listen-Endpoints bereits in Phase 1 geliefert
Nightly-Job: Live-Aggregation imaddResult()-Pfad viaonConflictDoUpdategenügt für Phase 2.
Phase 3a — Sync Research Agents ✅ (2026-04-17)
- Provider-Adapter (via direct HTTP, keine SDK-Deps):
PerplexitySonarProvider(4 Modelle: sonar, sonar-pro, sonar-reasoning, sonar-deep-research)ClaudeWebSearchProvider(Anthropic Messages API mitweb_search_20250305Tool)OpenAIResponsesProvider(OpenAI Responses API mitweb_search_previewTool)GeminiGroundingProvider(Google GenAI v1beta mit Google-Search-Grounding)
POST /v1/research+POST /v1/research/compare- Agent-Auto-Router (
pickAgentwählt ersten Provider mit Key: perplexity → gemini → openai → claude → deep-research) - Agents in
/v1/providers+/v1/providers/healthintegriert
Phase 3b — Async + Migrationen (offen)
OpenAIDeepResearchProvider— async, via Job-Queue,GET /v1/research/tasks/:idPolling-Endpoint- Auto-Router für
conversational-Queries → Agent-Mode in/v1/search(aktuell separate Endpoints) - Migration:
apps/api/src/modules/news-research/routes.tswird zum dünnen Adapter aufmana-research - Migration:
services/mana-ai/src/planner/news-research-client.tsruft jetztmana-researchdirekt stattmana-api - Migration:
research_news-Tool bekommt Optiondepth: 'shallow' | 'deep';deepruft Agent-Mode - Altes
mana-api/news-research/*bleibt als Backward-Compat, logged Deprecation-Warning
Phase 4 — Research Lab UI (≈ 1 Woche)
Neues Frontend-Modul apps/mana/apps/web/src/lib/modules/research-lab/ (tier: beta).
- Routes:
/research-lab— Main: Query-Input + Provider-Multi-Select + Compare-Button/research-lab/runs— Liste gespeicherter Runs mit Filter (query type, datum, provider)/research-lab/runs/[id]— Side-by-Side-View: Columns pro Provider, Ratings, Notes, Export
- Module-Store:
researchLab.svelte.ts(Runs, Results, aktueller Vergleich) - Component:
ProviderPicker.svelte(gruppiert nach Kategorie, zeigt Kosten + Capabilities) - Component:
ResultCard.svelte(normalisierter Result + Rating-UI + "raw" Toggle) - Component:
ComparisonGrid.svelte - Settings-Integration:
/settings/research-providersfür BYO-Keys + Budgets - Tool-Schema erweitert:
research_newsmit neuen Options, neuesdeep_researchTool (propose-policy wegen höherer Kosten) - Tier-Gate:
beta+ für Research Lab,research_newsbleibtpublic+ - Registrierung in
apps/mana/apps/web/src/lib/app-registry/apps.ts - Seed-Handler für Demo-Runs in
apps/mana/apps/web/src/lib/data/seed-registry.ts
8. Offene Fragen
mana-creditsAPI-Shape: hat der Service schon Reserve/Commit/Refund oder nur Debit? Muss geprüft werden in Phase 1 Task 0.- Rate-Limiting: separate per-User Rate-Limits pro Provider (damit ein Power-User nicht das Quota für alle aufbraucht)? Meine Tendenz: via
provider_configs.dailyBudgetCreditsreicht als Soft-Limit. - Export-Format für Eval-Runs: JSON-Download reicht, oder brauchen wir CSV/Markdown-Export für manuelle Analyse?
- Ollama als Agent? Wir könnten lokale LLMs mit Tool-Use-Capabilities (z.B. Llama 3.1 tool calling + eigener search-tool-loop) als "self-hosted agent" anbieten — wäre konsistent mit Self-Hosting-Positioning. Phase 5-Kandidat.
- Caching-TTL pro Provider: News sollten kürzer cachen als akademische Papers. Default 1h, aber konfigurierbar pro Provider-Config.
- Kosten-Dashboard: wann bauen wir ein Admin-Dashboard für
provider_stats? Phase 4 oder später?
9. Verzeichnisstruktur (Vorschlag)
services/mana-research/
├── CLAUDE.md
├── package.json
├── tsconfig.json
├── Dockerfile
├── drizzle.config.ts
├── src/
│ ├── server.ts # Hono bootstrap
│ ├── config.ts # Env-Vars
│ ├── db/
│ │ ├── schema.ts
│ │ └── migrations/
│ ├── providers/
│ │ ├── index.ts # registerAll()
│ │ ├── types.ts # re-export @mana/shared-research
│ │ ├── pricing.ts # PROVIDER_PRICING map
│ │ ├── search/
│ │ │ ├── searxng.ts
│ │ │ ├── brave.ts
│ │ │ ├── tavily.ts
│ │ │ ├── exa.ts
│ │ │ ├── serper.ts
│ │ │ └── duckduckgo.ts
│ │ ├── extract/
│ │ │ ├── readability.ts
│ │ │ ├── jina-reader.ts
│ │ │ ├── firecrawl.ts
│ │ │ └── scrapingbee.ts
│ │ └── agent/
│ │ ├── perplexity-sonar.ts
│ │ ├── claude-web-search.ts
│ │ ├── openai-responses.ts
│ │ ├── gemini-grounding.ts
│ │ └── openai-deep-research.ts
│ ├── routes/
│ │ ├── search.ts
│ │ ├── extract.ts
│ │ ├── research.ts
│ │ ├── runs.ts
│ │ └── providers.ts
│ ├── middleware/
│ │ ├── credits.ts # Reserve/Commit/Refund
│ │ ├── byo-keys.ts # Key-Resolution
│ │ └── cache.ts # Redis wrapper
│ ├── router/
│ │ ├── classify.ts # Query → QueryType via mana-llm
│ │ └── auto-route.ts # QueryType → Provider-Chain
│ ├── storage/
│ │ ├── runs.ts # persist + retrieve
│ │ └── stats.ts # aggregation job
│ └── clients/
│ ├── mana-llm.ts
│ ├── mana-credits.ts
│ └── mana-auth.ts
└── tests/
├── providers/
└── integration/
packages/shared-research/
├── package.json
└── src/
├── index.ts
├── types.ts # SearchHit, ExtractedContent, AgentAnswer, ProviderMeta
├── options.ts # SearchOptions, ExtractOptions, AgentOptions
└── ids.ts # ProviderId union
10. Risiken
| Risiko | Wahrscheinlichkeit | Impact | Mitigation |
|---|---|---|---|
| Provider-Pricing ändert sich | Hoch | Mittel | pricing.ts updatebar ohne Redeploy, Warnung wenn Kosten > X |
| Provider-Key wird gebannt (z.B. Brave bei zu viel Traffic) | Mittel | Hoch | Multi-Provider-Fallback in Auto-Router, Health-Check |
mana-credits Reserve/Refund-Pattern nicht ready |
Mittel | Hoch | Phase 1 Task 0: verifizieren + ggf. implementieren vor allem anderen |
| Kosten-Explosion durch Fan-Out-Compare | Mittel | Hoch | Hartes Limit auf providers.length (max 5), UI-Warnung mit Kosten-Preview |
| BYO-Key-Leak durch Logs | Niedrig | Sehr hoch | Keys nur verschlüsselt in DB, Logger-Filter für bekannte Key-Patterns |
| LLM-Klassifikation falsch → schlechter Auto-Route | Mittel | Niedrig | User kann immer explizit Provider wählen, Klassifikation ist nur Hint |
11. Verwandte Dateien
Heute existierende Research-Pfade (werden in Phase 3 umgestellt):
apps/api/src/lib/search.ts—webSearch()Wrapperapps/api/src/modules/news-research/routes.ts—/discover,/search,/extractservices/mana-ai/src/planner/news-research-client.tsservices/mana-ai/src/cron/tick.ts:282— Pre-Planning Research Stepapps/mana/apps/web/src/lib/modules/news-research/tools.ts:48—research_newsToolservices/mana-search/— bleibt, wird SearXNG+Readability-Providerpackages/shared-rss/— bleibt unverändert
Neu zu erstellen:
services/mana-research/**packages/shared-research/**apps/mana/apps/web/src/lib/modules/research-lab/**apps/mana/apps/web/src/routes/(app)/research-lab/**