managarten/services/mana-research/src/index.ts
Till JS f10a95e842 feat(mana-research): add Gemini 3.1 Pro Deep Research async providers
- New providers gemini-deep-research + gemini-deep-research-max on the
  Interactions API (preview-04-2026). Submit/poll split, tier parameter
  selects between standard (~minutes, $1–3) and max (up to 60 min, $3–7).
- Parser matches the real response shape: flat `outputs` array of
  thought|text|image items, url_citation annotations without title,
  `usage.total_input_tokens` / `total_output_tokens`.
- Route generalisation: /v1/research/async accepts `provider` with
  default 'openai-deep-research' (backward compatible) and dispatches
  to the right submit/poll pair.
- New internal service-to-service endpoint /v1/internal/research/async
  gated by X-Service-Key + X-User-Id for credit accounting. Enables
  mana-ai to drive deep-research jobs on the mission owner's wallet
  without requiring a user JWT.
- Pricing: 300 credits (standard) / 1500 credits (max). Conservative
  markup over the ~$3/$7 ceiling so the first runs can't surprise us.
- Docs: AGENT_PROVIDER_IDS + pricing + env map + auto-router stay in
  sync; CLAUDE.md Phase 3b now current; API_KEYS.md references the
  new providers under GOOGLE_GENAI_API_KEY.

Verified with a real smoke test against the Gemini API: submit + poll
both succeed, completed response parsed cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:55:30 +02:00

117 lines
4.3 KiB
TypeScript

/**
* mana-research — Web Research Provider Orchestration
*
* Bundles search/extract/agent providers behind a unified interface.
* Phase 1: 4 search providers (SearXNG, DuckDuckGo, Brave, Tavily) with
* credits + cache + eval-run persistence.
*
* Port: 3068. See docs/plans/mana-research-service.md.
*/
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { loadConfig } from './config';
import { getDb } from './db/connection';
import { serviceErrorHandler } from '@mana/shared-hono';
import { jwtAuth } from './middleware/jwt-auth';
import { serviceAuth } from './middleware/service-auth';
import { healthRoutes } from './routes/health';
import { createSearchRoutes } from './routes/search';
import { createExtractRoutes } from './routes/extract';
import { createResearchRoutes } from './routes/research';
import { createInternalResearchRoutes } from './routes/internal-research';
import { createProvidersRoutes } from './routes/providers';
import { createRunsRoutes } from './routes/runs';
import { createProviderConfigRoutes } from './routes/provider-configs';
import { buildRegistry } from './providers/registry';
import { RunStorage } from './storage/runs';
import { ConfigStorage } from './storage/configs';
import { AsyncJobStorage } from './storage/async-jobs';
import { CreditsClient } from './clients/mana-credits';
import { ManaSearchClient } from './clients/mana-search';
import { ManaLlmClient } from './clients/mana-llm';
import { initCache } from './lib/cache';
// ─── Bootstrap ──────────────────────────────────────────────
const config = loadConfig();
const db = getDb(config.databaseUrl);
initCache(config.redisUrl);
const manaSearch = new ManaSearchClient(config.manaSearchUrl);
const manaLlm = new ManaLlmClient(config.manaLlmUrl);
const credits = new CreditsClient({
baseUrl: config.manaCreditsUrl,
serviceKey: config.serviceKey,
});
const runStorage = new RunStorage(db);
const configStorage = new ConfigStorage(db);
const asyncStorage = new AsyncJobStorage(db);
const registry = buildRegistry({ manaSearch });
const executorDeps = {
credits,
configs: configStorage,
config,
};
// ─── App ────────────────────────────────────────────────────
const app = new Hono();
app.onError(serviceErrorHandler);
app.use(
'*',
cors({
origin: config.cors.origins,
credentials: true,
})
);
// Health (no auth)
app.route('/health', healthRoutes);
// Metrics stub (no auth) — will be populated in Phase 2 with prometheus-style output
app.get('/metrics', (c) => c.text('# mana-research metrics stub\n'));
// Providers catalog (no auth — callers often query this to build UIs)
app.route('/api/v1/providers', createProvidersRoutes(registry, config));
// User-facing research (JWT auth)
app.use('/api/v1/search/*', jwtAuth(config.manaAuthUrl));
app.route(
'/api/v1/search',
createSearchRoutes(registry, runStorage, executorDeps, config, manaLlm)
);
app.use('/api/v1/extract/*', jwtAuth(config.manaAuthUrl));
app.route('/api/v1/extract', createExtractRoutes(registry, runStorage, executorDeps, config));
app.use('/api/v1/research/*', jwtAuth(config.manaAuthUrl));
app.route(
'/api/v1/research',
createResearchRoutes(registry, runStorage, executorDeps, config, asyncStorage, credits)
);
app.use('/api/v1/runs/*', jwtAuth(config.manaAuthUrl));
app.route('/api/v1/runs', createRunsRoutes(runStorage));
app.use('/api/v1/provider-configs/*', jwtAuth(config.manaAuthUrl));
app.route('/api/v1/provider-configs', createProviderConfigRoutes(db));
// Service-to-service (X-Service-Key auth). Callers pass the target user
// in X-User-Id so credit accounting still lands on the right wallet.
app.use('/api/v1/internal/*', serviceAuth(config.serviceKey));
app.get('/api/v1/internal/health', (c) => c.json({ ok: true }));
app.route('/api/v1/internal/research', createInternalResearchRoutes(config, asyncStorage, credits));
// ─── Start ──────────────────────────────────────────────────
console.log(`mana-research starting on port ${config.port}...`);
export default {
port: config.port,
fetch: app.fetch,
};