feat(translate-mcp): scaffold per-tenant translation MCP (CF-3122)
Node.js MCP server exposing translate/search_tm/upsert_glossary/record_correction over StreamableHTTP on :9222. Routes translation calls to gemini-2.5-flash via LiteLLM, augmented with per-tenant TM + glossary + tone profile from the SmartTranslate pgvector DB. Schema migration in sql/001_schema.sql already applied to smarttranslate DB. Fleet registration in Infrastructure/litellm/config.yaml. Refs: CF-3122 CF-3123 CF-3124 CF-3125
This commit is contained in:
83
sql/001_schema.sql
Normal file
83
sql/001_schema.sql
Normal file
@@ -0,0 +1,83 @@
|
||||
-- CF-3124: add tenant discriminator + tenant_profile to SmartTranslate DB
|
||||
-- Safe to run multiple times; migrations wrapped in IF NOT EXISTS.
|
||||
-- Rollback at bottom of file (commented out).
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
|
||||
-- translation_memory: tenant column
|
||||
ALTER TABLE translation_memory
|
||||
ADD COLUMN IF NOT EXISTS tenant VARCHAR(64);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_tm_tenant_lang
|
||||
ON translation_memory (tenant, source_lang, target_lang);
|
||||
|
||||
-- glossary_terms: tenant column
|
||||
ALTER TABLE glossary_terms
|
||||
ADD COLUMN IF NOT EXISTS tenant VARCHAR(64);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_glossary_tenant_src
|
||||
ON glossary_terms (tenant, LOWER(source_term), target_lang);
|
||||
|
||||
-- Per-tenant uniqueness. The existing uq_glossary_term_ci_global (WHERE customer_id
|
||||
-- IS NULL) would collide across tenants for NULL-customer rows — redefine to exclude
|
||||
-- tenant'd rows, and add a parallel partial unique index for tenant'd rows.
|
||||
DROP INDEX IF EXISTS uq_glossary_term_ci_global;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_glossary_term_ci_global
|
||||
ON glossary_terms (LOWER(source_term), target_lang, COALESCE(context_hint, ''))
|
||||
WHERE customer_id IS NULL AND tenant IS NULL;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_glossary_term_tenant
|
||||
ON glossary_terms (tenant, LOWER(source_term), target_lang, COALESCE(context_hint, ''))
|
||||
WHERE tenant IS NOT NULL;
|
||||
|
||||
-- tenant_profile: new table
|
||||
CREATE TABLE IF NOT EXISTS tenant_profile (
|
||||
tenant VARCHAR(64) PRIMARY KEY,
|
||||
tone VARCHAR(32), -- professional / friendly / technical / marketing / editorial
|
||||
formality VARCHAR(16), -- formal / informal / neutral
|
||||
industry VARCHAR(64),
|
||||
base_rules TEXT, -- free-form system prompt fragment
|
||||
forbidden_terms TEXT[] DEFAULT '{}', -- e.g. {'AdGuardHome','Hetzner'} for vpn-marketing
|
||||
preferred_author_voice TEXT, -- e.g. "wir/nous (author voice ok, never Sie/du/vous)"
|
||||
direct_address_allowed BOOLEAN DEFAULT TRUE,
|
||||
default_source_lang VARCHAR(8),
|
||||
default_target_langs VARCHAR(8)[] DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
-- Seed known tenants (idempotent upsert on tenant PK)
|
||||
INSERT INTO tenant_profile (tenant, tone, formality, industry, base_rules, forbidden_terms, preferred_author_voice, direct_address_allowed, default_source_lang, default_target_langs)
|
||||
VALUES
|
||||
('ifk', 'editorial', 'neutral', 'kids-education',
|
||||
'Neutral, parent-facing tone. Refer to "Eltern", "das Kind", "les parents", "parents" rather than addressing the reader. Author voice "wir" / "nous" is OK. Do not append sales/CTA blocks; inline mentions only.',
|
||||
'{}'::text[], 'wir/nous (author voice only)', false, 'en', ARRAY['de','fr']),
|
||||
('vpn-marketing', 'marketing', 'neutral', 'consumer-vpn',
|
||||
'Consumer privacy/VPN marketing. Describe blocklists qualitatively (curated HaGeZi/OISD/Cloudflare-top-10k allowlist, ~15-30k DE-optimized entries). Use "Cloud-Server in Deutschland" instead of naming the hoster.',
|
||||
ARRAY['AdGuardHome','Hetzner'], NULL, true, 'en', ARRAY['de','fr']),
|
||||
('clicksports.de', 'professional', 'formal', 'hosting-msp',
|
||||
'B2B hosting/MSP tone. Always "Service" never "Support". Update-safe WHMCS context; avoid internal ticket jargon in customer-facing strings.',
|
||||
ARRAY['Support'], NULL, true, 'de', ARRAY['en']),
|
||||
('etoro', 'professional', 'neutral', 'retail-trading',
|
||||
'Retail trading/copy-trading tone. Factual, compliance-aware. No exaggerated performance claims. Use official instrument names.',
|
||||
'{}'::text[], NULL, true, 'en', ARRAY['de','fr','es','it']),
|
||||
('matrixhost', 'friendly', 'neutral', 'matrix-hosting',
|
||||
'Matrix chat hosting SaaS tone. Technical-but-approachable; family-first angle.',
|
||||
'{}'::text[], NULL, true, 'en', ARRAY['de']),
|
||||
('agiliton', 'professional', 'formal', 'msp',
|
||||
'Agiliton corporate voice. Short sentences, German engineering precision.',
|
||||
'{}'::text[], NULL, true, 'de', ARRAY['en'])
|
||||
ON CONFLICT (tenant) DO NOTHING;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- ROLLBACK (run manually if needed):
|
||||
-- BEGIN;
|
||||
-- DROP TABLE IF EXISTS tenant_profile;
|
||||
-- DROP INDEX IF EXISTS ix_glossary_tenant_src;
|
||||
-- DROP INDEX IF EXISTS ix_tm_tenant_lang;
|
||||
-- ALTER TABLE glossary_terms DROP COLUMN IF EXISTS tenant;
|
||||
-- ALTER TABLE translation_memory DROP COLUMN IF EXISTS tenant;
|
||||
-- COMMIT;
|
||||
Reference in New Issue
Block a user