From f93e651ffc17a67390a34ce733ecbc074f297d52 Mon Sep 17 00:00:00 2001 From: TenzDelek Date: Wed, 1 Apr 2026 11:00:41 +0530 Subject: [PATCH 1/6] response model --- api/ai/ai_response_model.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/api/ai/ai_response_model.py b/api/ai/ai_response_model.py index 4c4faed..17cd943 100644 --- a/api/ai/ai_response_model.py +++ b/api/ai/ai_response_model.py @@ -43,8 +43,16 @@ class StreamRequest(BaseModel): offset: int = 0 instruction: Optional[str] = None +class FuzzyMatch(BaseModel): + source_text: str + target_text: str + score: float + + class WorkflowResult(BaseModel): output_text: str + from_memory: bool = False + fuzzy_matches: List[FuzzyMatch] = [] class ResponseMetadata(BaseModel): From 37408153242f081110e814ebed4d26dc7eddff9d Mon Sep 17 00:00:00 2001 From: TenzDelek Date: Wed, 1 Apr 2026 11:00:52 +0530 Subject: [PATCH 2/6] base service --- api/ai/ai_service.py | 133 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 110 insertions(+), 23 deletions(-) diff --git a/api/ai/ai_service.py b/api/ai/ai_service.py index d603b0a..cb7ca1a 100644 --- a/api/ai/ai_service.py +++ b/api/ai/ai_service.py @@ -1,4 +1,6 @@ import asyncio +import logging +from datetime import datetime, timezone from typing import AsyncGenerator from api.Assistant.assistant_repository import get_assistant_by_id_repository @@ -13,14 +15,23 @@ ResponseMetadata, AvailableModelsResponse, ModelInfo, - EnhanceResponse + EnhanceResponse, + FuzzyMatch, ) from api.db.pg_database import SessionLocal from api.llm.router import get_model_router from api.external_api import get_related_segment_ids, get_segment_content from api.ai.prompts import ENHANCE_META_PROMPT +from api.translation_memory.tm_model import TranslationMemory +from api.translation_memory.tm_repository import ( + find_exact_match, + find_fuzzy_matches, + batch_create_tm_entries, +) from fastapi import HTTPException +logger = logging.getLogger(__name__) + def build_workflow_request(db_session, assistant_id, target_language, prompt, segments, model, instruction=None) -> WorkflowRequest: assistant_detail = get_assistant_by_id_repository(db_session, assistant_id) @@ -65,10 +76,59 @@ def validate_model(model: str) -> None: async def run_workflow_service(assistant_id, target_language, prompt, segments, model, offset=0, instruction=None): validate_model(model) - + + tm_hits = {} + fuzzy_cache = {} + texts_for_llm = [] + + if target_language: + with SessionLocal() as db_session: + for idx, source_text in enumerate(prompt): + exact = find_exact_match( + db_session, assistant_id, source_text, target_language + ) + if exact: + tm_hits[idx] = exact.target_text + continue + + rows = find_fuzzy_matches( + db_session, assistant_id, source_text, target_language + ) + fuzzy_cache[idx] = [ + FuzzyMatch( + source_text=row.source_text, + target_text=row.target_text, + score=round(float(row.score), 4), + ) + for row in rows + ] + texts_for_llm.append((idx, source_text)) + else: + texts_for_llm = list(enumerate(prompt)) + + if not texts_for_llm: + now = datetime.now(timezone.utc).isoformat() + return StreamResponse( + results=[ + WorkflowResult( + output_text=tm_hits[idx], from_memory=True + ) + for idx in range(len(prompt)) + ], + metadata=ResponseMetadata( + initialized_at=now, + total_batches=0, + completed_at=now, + total_processing_time=0.0, + ), + errors=[], + ) + + llm_prompts = [text for _, text in texts_for_llm] + with SessionLocal() as db_session: workflow_request = build_workflow_request( - db_session, assistant_id, target_language, prompt, segments, model, instruction + db_session, assistant_id, target_language, llm_prompts, segments, model, instruction ) if workflow_request.instance_ids and workflow_request.segments: all_segment_ids = [] @@ -97,29 +157,56 @@ async def run_workflow_service(assistant_id, target_language, prompt, segments, workflow_request.contexts.append( ContextRequest(content=content) ) - + workflow_response = await run_workflow(workflow_request) - - results = [ - WorkflowResult(output_text=result.output_text) - for result in workflow_response.get("final_results", []) - ] - + llm_results = workflow_response.get("final_results", []) + + if target_language and llm_results: + try: + with SessionLocal() as db_session: + new_entries = [ + TranslationMemory( + assistant_id=assistant_id, + source_text=source_text, + target_text=result.output_text, + target_language=target_language, + ) + for (_, source_text), result in zip(texts_for_llm, llm_results) + ] + batch_create_tm_entries(db_session, new_entries) + except Exception as e: + logger.warning(f"Failed to save translations to TM: {e}") + + llm_iter = iter(llm_results) + merged_results = [] + for idx in range(len(prompt)): + if idx in tm_hits: + merged_results.append( + WorkflowResult( + output_text=tm_hits[idx], from_memory=True + ) + ) + else: + llm_result = next(llm_iter) + merged_results.append( + WorkflowResult( + output_text=llm_result.output_text, + from_memory=False, + fuzzy_matches=fuzzy_cache.get(idx, []), + ) + ) + workflow_metadata = workflow_response.get("metadata", {}) - response_metadata = ResponseMetadata( - initialized_at=workflow_metadata.get("initialized_at"), - total_batches=workflow_metadata.get("total_batches"), - completed_at=workflow_metadata.get("completed_at"), - total_processing_time=workflow_metadata.get("total_processing_time") + return StreamResponse( + results=merged_results, + metadata=ResponseMetadata( + initialized_at=workflow_metadata.get("initialized_at"), + total_batches=workflow_metadata.get("total_batches"), + completed_at=workflow_metadata.get("completed_at"), + total_processing_time=workflow_metadata.get("total_processing_time"), + ), + errors=workflow_response.get("errors", []), ) - - response = StreamResponse( - results=results, - metadata=response_metadata, - errors=workflow_response.get("errors", []) - ) - - return response async def stream_workflow_service( From 451918756295625f260223f351f5db00bc4ef189 Mon Sep 17 00:00:00 2001 From: TenzDelek Date: Wed, 1 Apr 2026 11:00:57 +0530 Subject: [PATCH 3/6] rest --- .../tm_model.py | 0 api/translation_memory/tm_repository.py | 119 ++++++++++++++++++ 2 files changed, 119 insertions(+) rename api/{translation-memory => translation_memory}/tm_model.py (100%) create mode 100644 api/translation_memory/tm_repository.py diff --git a/api/translation-memory/tm_model.py b/api/translation_memory/tm_model.py similarity index 100% rename from api/translation-memory/tm_model.py rename to api/translation_memory/tm_model.py diff --git a/api/translation_memory/tm_repository.py b/api/translation_memory/tm_repository.py new file mode 100644 index 0000000..47a275e --- /dev/null +++ b/api/translation_memory/tm_repository.py @@ -0,0 +1,119 @@ +from sqlalchemy.orm import Session +from sqlalchemy import text +from api.translation_memory.tm_model import TranslationMemory +from uuid import UUID +from typing import List, Optional, Tuple +from fastapi import HTTPException, status +import logging + +logger = logging.getLogger(__name__) + + +def find_exact_match( + db: Session, assistant_id: UUID, source_text: str, target_language: str +) -> Optional[TranslationMemory]: + return db.query(TranslationMemory).filter( + TranslationMemory.assistant_id == assistant_id, + TranslationMemory.source_text == source_text, + TranslationMemory.target_language == target_language, + ).first() + + +def find_fuzzy_matches( + db: Session, + assistant_id: UUID, + source_text: str, + target_language: str, + limit: int = 5, + threshold: float = 0.3, +) -> list: + results = db.execute( + text(""" + SELECT source_text, target_text, + similarity(source_text, :source_text) AS score + FROM translation_memory + WHERE assistant_id = :assistant_id + AND target_language = :target_language + AND similarity(source_text, :source_text) > :threshold + AND source_text != :source_text + ORDER BY score DESC + LIMIT :limit + """), + { + "assistant_id": str(assistant_id), + "source_text": source_text, + "target_language": target_language, + "threshold": threshold, + "limit": limit, + }, + ).fetchall() + return results + + +def create_tm_entry(db: Session, tm: TranslationMemory) -> TranslationMemory: + try: + db.add(tm) + db.commit() + db.refresh(tm) + return tm + except Exception as e: + db.rollback() + logger.error(f"Error creating TM entry: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to create translation memory entry", + ) + + +def batch_create_tm_entries( + db: Session, entries: List[TranslationMemory] +) -> List[TranslationMemory]: + try: + db.add_all(entries) + db.commit() + for entry in entries: + db.refresh(entry) + return entries + except Exception as e: + db.rollback() + logger.error(f"Error batch creating TM entries: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to batch create translation memory entries", + ) + + +def get_tm_entries( + db: Session, + assistant_id: UUID, + target_language: Optional[str] = None, + skip: int = 0, + limit: int = 20, +) -> Tuple[List[TranslationMemory], int]: + query = db.query(TranslationMemory).filter( + TranslationMemory.assistant_id == assistant_id + ) + if target_language: + query = query.filter(TranslationMemory.target_language == target_language) + total = query.count() + entries = query.order_by(TranslationMemory.created_at.desc()).offset(skip).limit(limit).all() + return entries, total + + +def delete_tm_entry(db: Session, tm_id: UUID) -> None: + entry = db.query(TranslationMemory).filter(TranslationMemory.id == tm_id).first() + if not entry: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Translation memory entry not found", + ) + try: + db.delete(entry) + db.commit() + except Exception as e: + db.rollback() + logger.error(f"Error deleting TM entry: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to delete translation memory entry", + ) From 7b746a80f1a12ff69b0857c9427a1a379e583f48 Mon Sep 17 00:00:00 2001 From: TenzDelek Date: Wed, 1 Apr 2026 11:03:14 +0530 Subject: [PATCH 4/6] remove unwanted service --- api/translation_memory/tm_repository.py | 54 +------------------------ 1 file changed, 1 insertion(+), 53 deletions(-) diff --git a/api/translation_memory/tm_repository.py b/api/translation_memory/tm_repository.py index 47a275e..a6c08a9 100644 --- a/api/translation_memory/tm_repository.py +++ b/api/translation_memory/tm_repository.py @@ -2,7 +2,7 @@ from sqlalchemy import text from api.translation_memory.tm_model import TranslationMemory from uuid import UUID -from typing import List, Optional, Tuple +from typing import List, Optional from fastapi import HTTPException, status import logging @@ -49,22 +49,6 @@ def find_fuzzy_matches( ).fetchall() return results - -def create_tm_entry(db: Session, tm: TranslationMemory) -> TranslationMemory: - try: - db.add(tm) - db.commit() - db.refresh(tm) - return tm - except Exception as e: - db.rollback() - logger.error(f"Error creating TM entry: {e}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to create translation memory entry", - ) - - def batch_create_tm_entries( db: Session, entries: List[TranslationMemory] ) -> List[TranslationMemory]: @@ -81,39 +65,3 @@ def batch_create_tm_entries( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to batch create translation memory entries", ) - - -def get_tm_entries( - db: Session, - assistant_id: UUID, - target_language: Optional[str] = None, - skip: int = 0, - limit: int = 20, -) -> Tuple[List[TranslationMemory], int]: - query = db.query(TranslationMemory).filter( - TranslationMemory.assistant_id == assistant_id - ) - if target_language: - query = query.filter(TranslationMemory.target_language == target_language) - total = query.count() - entries = query.order_by(TranslationMemory.created_at.desc()).offset(skip).limit(limit).all() - return entries, total - - -def delete_tm_entry(db: Session, tm_id: UUID) -> None: - entry = db.query(TranslationMemory).filter(TranslationMemory.id == tm_id).first() - if not entry: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Translation memory entry not found", - ) - try: - db.delete(entry) - db.commit() - except Exception as e: - db.rollback() - logger.error(f"Error deleting TM entry: {e}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to delete translation memory entry", - ) From a051b5ecd68cc9f6f28b8fa838d9751315706e38 Mon Sep 17 00:00:00 2001 From: TenzDelek Date: Wed, 1 Apr 2026 11:21:25 +0530 Subject: [PATCH 5/6] code cleanup --- api/ai/ai_service.py | 90 ++++++++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 36 deletions(-) diff --git a/api/ai/ai_service.py b/api/ai/ai_service.py index cb7ca1a..18f7f2e 100644 --- a/api/ai/ai_service.py +++ b/api/ai/ai_service.py @@ -9,7 +9,6 @@ from api.langgraph.workflow_stream import stream_workflow_events from api.ai.ai_response_model import ( WorkflowRequest, - SegmentRequest, StreamResponse, WorkflowResult, ResponseMetadata, @@ -74,9 +73,7 @@ def validate_model(model: str) -> None: ) -async def run_workflow_service(assistant_id, target_language, prompt, segments, model, offset=0, instruction=None): - validate_model(model) - +def _lookup_translation_memory(assistant_id, prompt, target_language): tm_hits = {} fuzzy_cache = {} texts_for_llm = [] @@ -106,6 +103,53 @@ async def run_workflow_service(assistant_id, target_language, prompt, segments, else: texts_for_llm = list(enumerate(prompt)) + return tm_hits, fuzzy_cache, texts_for_llm + + +def _save_translations_to_memory(assistant_id, target_language, texts_for_llm, llm_results): + try: + with SessionLocal() as db_session: + new_entries = [ + TranslationMemory( + assistant_id=assistant_id, + source_text=source_text, + target_text=result.output_text, + target_language=target_language, + ) + for (_, source_text), result in zip(texts_for_llm, llm_results) + ] + batch_create_tm_entries(db_session, new_entries) + except Exception as e: + logger.warning(f"Failed to save translations to TM: {e}") + + +def _merge_tm_and_llm_results(prompt, tm_hits, fuzzy_cache, llm_results): + llm_iter = iter(llm_results) + merged = [] + for idx in range(len(prompt)): + if idx in tm_hits: + merged.append( + WorkflowResult(output_text=tm_hits[idx], from_memory=True) + ) + else: + llm_result = next(llm_iter) + merged.append( + WorkflowResult( + output_text=llm_result.output_text, + from_memory=False, + fuzzy_matches=fuzzy_cache.get(idx, []), + ) + ) + return merged + + +async def run_workflow_service(assistant_id, target_language, prompt, segments, model, offset=0, instruction=None): + validate_model(model) + + tm_hits, fuzzy_cache, texts_for_llm = _lookup_translation_memory( + assistant_id, prompt, target_language + ) + if not texts_for_llm: now = datetime.now(timezone.utc).isoformat() return StreamResponse( @@ -162,39 +206,13 @@ async def run_workflow_service(assistant_id, target_language, prompt, segments, llm_results = workflow_response.get("final_results", []) if target_language and llm_results: - try: - with SessionLocal() as db_session: - new_entries = [ - TranslationMemory( - assistant_id=assistant_id, - source_text=source_text, - target_text=result.output_text, - target_language=target_language, - ) - for (_, source_text), result in zip(texts_for_llm, llm_results) - ] - batch_create_tm_entries(db_session, new_entries) - except Exception as e: - logger.warning(f"Failed to save translations to TM: {e}") + _save_translations_to_memory( + assistant_id, target_language, texts_for_llm, llm_results + ) - llm_iter = iter(llm_results) - merged_results = [] - for idx in range(len(prompt)): - if idx in tm_hits: - merged_results.append( - WorkflowResult( - output_text=tm_hits[idx], from_memory=True - ) - ) - else: - llm_result = next(llm_iter) - merged_results.append( - WorkflowResult( - output_text=llm_result.output_text, - from_memory=False, - fuzzy_matches=fuzzy_cache.get(idx, []), - ) - ) + merged_results = _merge_tm_and_llm_results( + prompt, tm_hits, fuzzy_cache, llm_results + ) workflow_metadata = workflow_response.get("metadata", {}) return StreamResponse( From aaedf1021a40b3d9dd80542c8b5e8d7d1df51e8c Mon Sep 17 00:00:00 2001 From: TenzDelek Date: Wed, 1 Apr 2026 11:38:12 +0530 Subject: [PATCH 6/6] migration --- .../351aedf7ebe6_translation_memory_schema.py | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/migrations/versions/351aedf7ebe6_translation_memory_schema.py b/migrations/versions/351aedf7ebe6_translation_memory_schema.py index 6d0cf03..f2f648f 100644 --- a/migrations/versions/351aedf7ebe6_translation_memory_schema.py +++ b/migrations/versions/351aedf7ebe6_translation_memory_schema.py @@ -11,7 +11,6 @@ import sqlalchemy as sa -# revision identifiers, used by Alembic. revision: str = '351aedf7ebe6' down_revision: Union[str, Sequence[str], None] = '8ef199eeb359' branch_labels: Union[str, Sequence[str], None] = None @@ -19,14 +18,38 @@ def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### + op.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm") + + op.create_table( + "translation_memory", + sa.Column("id", sa.UUID(), primary_key=True), + sa.Column( + "assistant_id", + sa.UUID(), + sa.ForeignKey("assistant.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column("source_text", sa.Text(), nullable=False), + sa.Column("target_text", sa.Text(), nullable=False), + sa.Column("target_language", sa.String(255), nullable=False), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.func.now(), + ), + ) + + op.execute( + """ + CREATE INDEX idx_translation_memory_source_trgm + ON translation_memory + USING gist (source_text gist_trgm_ops) + """ + ) def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### + op.execute("DROP INDEX IF EXISTS idx_translation_memory_source_trgm") + op.drop_table("translation_memory") + op.execute("DROP EXTENSION IF EXISTS pg_trgm")