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

28 KiB
Raw Permalink Blame History

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:

  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.

// 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.

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 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.

// 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
  • 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)

  • 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)
  • POST /v1/extract + POST /v1/extract/compare
  • Query-Klassifikation: classify.ts mit Regex-Fast-Path + optionalem mana-llm-Call (useLlmClassifier: true)
  • Auto-Router in POST /v1/search (wenn provider nicht 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 im addResult()-Pfad via onConflictDoUpdate genü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 mit web_search_20250305 Tool)
    • OpenAIResponsesProvider (OpenAI Responses API mit web_search_preview Tool)
    • GeminiGroundingProvider (Google GenAI v1beta mit Google-Search-Grounding)
  • POST /v1/research + POST /v1/research/compare
  • Agent-Auto-Router (pickAgent wählt ersten Provider mit Key: perplexity → gemini → openai → claude → deep-research)
  • 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.tswebSearch() 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:48research_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/**