managarten/docs/plans/mana-research-service.md
Till JS 49f315f6be feat(research): Phase 3a — 4 sync research agents
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>
2026-04-17 15:06:12 +02:00

584 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`](../reports/web-research-capabilities.md)
**Verwandtes Modul:** [`docs/plans/news-research-module.md`](./news-research-module.md) (das bestehende RSS-Modul, wird in Phase 3 auf den neuen Service migriert)
## Ziel
Eine zentrale Research-Schicht, die:
1. **Viele Anbieter bündelt** hinter einer gemeinsamen Schnittstelle (Search, Extract, Agent).
2. **Side-by-Side-Vergleich** erlaubt (gleiche Query → N Provider parallel, normalisierte + rohe Ergebnisse in PostgreSQL).
3. **Auto-Routing** nach Query-Typ macht (News → Tavily, Paper → Exa, komplexe Frage → Perplexity, …).
4. **Pay-per-use-only**: keine Services mit Monats-Abo, nur API-Call-basierte Abrechnung.
5. **mana-credits** integriert ist — jeder Call kostet je nach Provider + Operation.
6. **Hybrid-Keys** unterstützt — Server-Keys als Default (charged via credits), BYO-Keys pro User optional (kein Credit-Verbrauch).
7. **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-ai`** behält seinen eigenen NewsResearchClient, wird in Phase 3 auf `mana-research` umgestellt.
---
## 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-hono` authMiddleware 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.
```ts
// 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 | 310 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 | 50500 je nach Modell |
| `claude-web-search` | Claude-API mit server-seitigem `web_search`-Tool | Token-based + $10/1k searches | Ja (Anthropic) | 100300 |
| `openai-responses` | OpenAI Responses API mit `web_search_preview`-Tool | Token-based + per-tool-call | Ja (OpenAI) | 100300 |
| `gemini-grounding` | Gemini + Google Search Grounding | Token-based + per-grounding-query | Ja (Google) | 80200 |
| `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`.
```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
```ts
// 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 aus `mana-auth`)
- Tabelle `research.provider_configs` mit `userId` + `providerId` unique 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.development` dokumentiert + `scripts/generate-env.mjs` ergänzt
---
## 6. Query-Klassifikation & Auto-Router
Für `mode: 'auto'`: Query wird via `mana-llm` (gemma3:4b) klassifiziert → bester Provider je Typ.
```ts
// 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-research` für Provider-Typen
- [ ] DB-Migration: `research` schema (4 Tabellen)
- [ ] Provider-Adapter:
- `SearXNGProvider` (wrapt `mana-search`)
- `BraveSearchProvider`
- `TavilyProvider`
- `DuckDuckGoProvider` (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`](../PORT_SCHEMA.md)
- [ ] Env-Vars: `BRAVE_API_KEY`, `TAVILY_API_KEY` in `.env.development`
- [ ] `services/mana-research/CLAUDE.md`
- [ ] Eintrag im Root-CLAUDE.md "Active services"
- [ ] **Falls nötig:** Erweiterung `mana-credits` um Reserve/Commit/Refund
- [ ] Integration-Tests: `tests/search-providers.spec.ts` (Mock-HTTP)
### Phase 2 — Extraction + semantische Suche ✅ (2026-04-17)
- [x] Provider-Adapter:
- `ReadabilityProvider` (wrapt `mana-search /extract`)
- `JinaReaderProvider` (zero-auth, optionaler Key für höheres Rate-Limit)
- `FirecrawlProvider` (PAYG + self-hostbar via `FIRECRAWL_API_URL`)
- ~~`ScrapingBeeProvider`~~ — deferred: liefert raw HTML, braucht zusätzlichen Readability-Pass. Wird als Phase-3-Addition behandelt.
- `ExaProvider` (semantische Suche in Phase 2 gelandet)
- `SerperProvider` (Google SERP als JSON)
- [x] `POST /v1/extract` + `POST /v1/extract/compare`
- [x] Query-Klassifikation: `classify.ts` mit Regex-Fast-Path + optionalem `mana-llm`-Call (`useLlmClassifier: true`)
- [x] Auto-Router in `POST /v1/search` (wenn `provider` nicht gesetzt) + Auto-Router für Extract
- [x] Provider-Health-Check-Endpoint `GET /v1/providers/health`
- [x] Run-Listen-Endpoints bereits in Phase 1 geliefert
- [x] ~~Nightly-Job~~: Live-Aggregation im `addResult()`-Pfad via `onConflictDoUpdate` genügt für Phase 2.
### Phase 3a — Sync Research Agents ✅ (2026-04-17)
- [x] Provider-Adapter (via direct HTTP, keine SDK-Deps):
- `PerplexitySonarProvider` (4 Modelle: sonar, sonar-pro, sonar-reasoning, sonar-deep-research)
- `ClaudeWebSearchProvider` (Anthropic Messages API mit `web_search_20250305` Tool)
- `OpenAIResponsesProvider` (OpenAI Responses API mit `web_search_preview` Tool)
- `GeminiGroundingProvider` (Google GenAI v1beta mit Google-Search-Grounding)
- [x] `POST /v1/research` + `POST /v1/research/compare`
- [x] Agent-Auto-Router (`pickAgent` wählt ersten Provider mit Key: perplexity → gemini → openai → claude → deep-research)
- [x] Agents in `/v1/providers` + `/v1/providers/health` integriert
### Phase 3b — Async + Migrationen (offen)
- [ ] `OpenAIDeepResearchProvider` — async, via Job-Queue, `GET /v1/research/tasks/:id` Polling-Endpoint
- [ ] Auto-Router für `conversational`-Queries → Agent-Mode in `/v1/search` (aktuell separate Endpoints)
- [ ] **Migration:** `apps/api/src/modules/news-research/routes.ts` wird zum dünnen Adapter auf `mana-research`
- [ ] **Migration:** `services/mana-ai/src/planner/news-research-client.ts` ruft jetzt `mana-research` direkt statt `mana-api`
- [ ] **Migration:** `research_news`-Tool bekommt Option `depth: 'shallow' | 'deep'`; `deep` ruft 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-providers` für BYO-Keys + Budgets
- [ ] Tool-Schema erweitert: `research_news` mit neuen Options, neues `deep_research` Tool (propose-policy wegen höherer Kosten)
- [ ] Tier-Gate: `beta`+ für Research Lab, `research_news` bleibt `public`+
- [ ] 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-credits` API-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.dailyBudgetCredits` reicht 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()` Wrapper
- `apps/api/src/modules/news-research/routes.ts``/discover`, `/search`, `/extract`
- `services/mana-ai/src/planner/news-research-client.ts`
- `services/mana-ai/src/cron/tick.ts:282` — Pre-Planning Research Step
- `apps/mana/apps/web/src/lib/modules/news-research/tools.ts:48``research_news` Tool
- `services/mana-search/` — bleibt, wird SearXNG+Readability-Provider
- `packages/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/**`