mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
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>
584 lines
28 KiB
Markdown
584 lines
28 KiB
Markdown
# 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 | 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`.
|
||
|
||
```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/**`
|