feat(cowork-service): H.10 extraction proxy routes + EXTRACTION_SERVICE_URL config
Add extraction proxy routes to cowork-service for hybrid document processing: - POST /api/extract — proxy to extraction-service POST /extract - GET /api/extract/models — list available extraction models - modules/extraction/routes.ts — proxy routes with error handling - lib/config.ts — add EXTRACTION_SERVICE_URL (default: http://localhost:4005) - server.ts — register extractionRoutes (7 route modules total) - server.test.ts — add extraction routes mock, update register count to 7 Hybrid approach: Rust skills server handles local doc processing (PDF/xlsx/docx/pptx) inside Docker sandbox, extraction-service provides AI-powered entity extraction. 57 tests passing, 9 test files, typecheck clean
This commit is contained in:
parent
88cc18549c
commit
62997bb1db
@ -22,6 +22,9 @@ const envSchema = baseBackendConfigSchema.extend({
|
||||
// Platform-service connection (for auth, flags, audit, etc.)
|
||||
PLATFORM_SERVICE_URL: z.string().default('http://localhost:4003'),
|
||||
|
||||
// Extraction-service connection (for AI-powered document extraction)
|
||||
EXTRACTION_SERVICE_URL: z.string().default('http://localhost:4005'),
|
||||
|
||||
// Rust runtime IPC — path to the cowork-orchestrator binary
|
||||
RUST_RUNTIME_BIN: z.string().default('cowork-orchestrator'),
|
||||
RUST_RUNTIME_TIMEOUT_MS: z.coerce.number().default(300_000),
|
||||
|
||||
62
services/cowork-service/src/modules/extraction/routes.ts
Normal file
62
services/cowork-service/src/modules/extraction/routes.ts
Normal file
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Extraction proxy routes — forward to extraction-service (port 4005).
|
||||
*
|
||||
* Hybrid approach: the Rust skills server handles local document processing
|
||||
* (PDF, xlsx, docx, pptx) inside the Docker sandbox, while extraction-service
|
||||
* provides AI-powered entity extraction and text analysis.
|
||||
*
|
||||
* This proxy lets the Tauri desktop access extraction-service via cowork-service.
|
||||
*/
|
||||
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { config } from '../../lib/config.js';
|
||||
import { PRODUCT_ID } from '../../lib/product-config.js';
|
||||
|
||||
export async function extractionRoutes(app: FastifyInstance) {
|
||||
const extractionUrl = config.EXTRACTION_SERVICE_URL;
|
||||
|
||||
// POST /api/extract — proxy to extraction-service POST /extract
|
||||
app.post('/api/extract', async (req, reply) => {
|
||||
try {
|
||||
const res = await fetch(`${extractionUrl}/extract`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-product-id': PRODUCT_ID,
|
||||
'x-request-id': req.id,
|
||||
},
|
||||
body: JSON.stringify(req.body),
|
||||
});
|
||||
if (!res.ok) {
|
||||
reply.code(res.status);
|
||||
return { error: `Extraction service returned ${res.status}` };
|
||||
}
|
||||
return res.json();
|
||||
} catch (err) {
|
||||
req.log.warn({ err }, 'Failed to proxy extraction request');
|
||||
reply.code(502);
|
||||
return { error: 'Extraction service unavailable' };
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/extract/models — list available extraction models
|
||||
app.get('/api/extract/models', async (req, reply) => {
|
||||
try {
|
||||
const res = await fetch(`${extractionUrl}/extract/models`, {
|
||||
headers: {
|
||||
'x-product-id': PRODUCT_ID,
|
||||
'x-request-id': req.id,
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
reply.code(res.status);
|
||||
return { error: `Extraction service returned ${res.status}` };
|
||||
}
|
||||
return res.json();
|
||||
} catch (err) {
|
||||
req.log.warn({ err }, 'Failed to proxy extraction models');
|
||||
reply.code(502);
|
||||
return { error: 'Extraction service unavailable' };
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -34,6 +34,7 @@ vi.mock('./lib/config.js', () => ({
|
||||
ANTHROPIC_API_KEY: undefined,
|
||||
OLLAMA_URL: 'http://localhost:11434/v1',
|
||||
OLLAMA_MODELS: undefined,
|
||||
EXTRACTION_SERVICE_URL: 'http://localhost:4005',
|
||||
},
|
||||
}));
|
||||
vi.mock('./lib/product-config.js', () => ({
|
||||
@ -73,6 +74,7 @@ vi.mock('./modules/llm/routes.js', () => ({ llmRoutes: vi.fn() }));
|
||||
vi.mock('./modules/audit/routes.js', () => ({ auditRoutes: vi.fn() }));
|
||||
vi.mock('./modules/usage/routes.js', () => ({ usageRoutes: vi.fn() }));
|
||||
vi.mock('./modules/notifications/routes.js', () => ({ notificationRoutes: vi.fn() }));
|
||||
vi.mock('./modules/extraction/routes.js', () => ({ extractionRoutes: vi.fn() }));
|
||||
|
||||
describe('cowork-service bootstrap', () => {
|
||||
beforeEach(() => {
|
||||
@ -92,9 +94,9 @@ describe('cowork-service bootstrap', () => {
|
||||
expect(opts.version).toBe('0.1.0');
|
||||
expect(opts.readiness).toBe(true);
|
||||
|
||||
// health + task + llm + audit + usage + notifications = 6 register calls + 1 JWT
|
||||
// health + task + llm + audit + usage + notifications + extraction = 7 register calls + 1 JWT
|
||||
expect(registerOptionalJwtContextMock).toHaveBeenCalledOnce();
|
||||
expect(appMock.register).toHaveBeenCalledTimes(6);
|
||||
expect(appMock.register).toHaveBeenCalledTimes(7);
|
||||
expect(startServiceMock).toHaveBeenCalledWith(appMock, { port: 4009, host: '0.0.0.0' });
|
||||
});
|
||||
});
|
||||
|
||||
@ -28,6 +28,7 @@ import { llmRoutes } from './modules/llm/routes.js';
|
||||
import { auditRoutes } from './modules/audit/routes.js';
|
||||
import { usageRoutes } from './modules/usage/routes.js';
|
||||
import { notificationRoutes } from './modules/notifications/routes.js';
|
||||
import { extractionRoutes } from './modules/extraction/routes.js';
|
||||
import type { JwtPayload } from './lib/request-context.js';
|
||||
|
||||
const jwtSecret = new TextEncoder().encode(config.JWT_SECRET);
|
||||
@ -60,6 +61,7 @@ await app.register(llmRoutes);
|
||||
await app.register(auditRoutes);
|
||||
await app.register(usageRoutes);
|
||||
await app.register(notificationRoutes);
|
||||
await app.register(extractionRoutes);
|
||||
|
||||
// Bootstrap endpoint (same pattern as FlowMonk, ActionTrail, etc.)
|
||||
app.get('/api/bootstrap', async () => ({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user