-
Notifications
You must be signed in to change notification settings - Fork 10
feat(llm): Introduce type safety checks #1089
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
934052b
438a894
3b9fd9f
3e44e65
a8f9594
1aa7919
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,8 @@ | |
|
|
||
| from fastapi import APIRouter, Depends, HTTPException | ||
| from opentelemetry import trace | ||
| from pydantic import TypeAdapter | ||
| from sqlmodel import Session | ||
|
|
||
| from app.api.deps import AuthContextDep, SessionDep | ||
| from app.api.permissions import Permission, require_permission | ||
|
|
@@ -19,13 +21,50 @@ | |
| LLMJobPublic, | ||
| JobStatus, | ||
| ) | ||
| from app.models.llm.response import LLMResponse, Usage | ||
| from app.models.llm.response import LLMOutput, LLMResponse, Usage | ||
| from app.services.llm.jobs import start_job | ||
| from app.utils import APIResponse, validate_callback_url, load_description | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| router = APIRouter(tags=["LLM"]) | ||
|
|
||
| _LLM_OUTPUT_ADAPTER: TypeAdapter[LLMOutput] = TypeAdapter(LLMOutput) | ||
|
|
||
|
|
||
| def _resolve_llm_output( | ||
| raw_content: dict, | ||
| project_id: int, | ||
| session: Session, | ||
| job_id: UUID, | ||
| ) -> LLMOutput | None: | ||
| """Parse the persisted `llm_call.content` dict into the typed LLMOutput, | ||
| presigning the audio URL in place first. | ||
|
|
||
| Persisted TTS content marks a not-yet-presigned S3 path with format="uri" — | ||
| not a valid AudioContent literal ("base64"/"url") — so that sentinel must be | ||
| resolved to a real "url" before the dict can validate into the typed model. | ||
| """ | ||
| inner = raw_content.get("content") | ||
| if ( | ||
| raw_content.get("type") == "audio" | ||
| and isinstance(inner, dict) | ||
| and inner.get("format") == "uri" | ||
| ): | ||
| s3_path = inner.get("value", "") | ||
| try: | ||
| storage = get_cloud_storage(session, project_id) | ||
| inner["value"] = storage.get_signed_url(s3_path, expires_in=3600) | ||
|
Prajna1999 marked this conversation as resolved.
|
||
| except Exception as e: | ||
| logger.warning( | ||
| f"[get_llm_call_status] Failed to generate presigned URL for audio: {e} | job_id={job_id}" | ||
| ) | ||
|
Comment on lines
+59
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Use the helper’s name in this log prefix. This warning originates in 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| inner["value"] = "" | ||
| inner["format"] = "url" | ||
|
|
||
| return _LLM_OUTPUT_ADAPTER.validate_python(raw_content) | ||
|
|
||
|
|
||
| llm_callback_router = APIRouter() | ||
|
|
||
|
|
||
|
|
@@ -155,32 +194,19 @@ def get_llm_call_status( | |
| # Get the first LLM call from the list which will be the only call for the job id | ||
| # since we initially won't be using this endpoint for llm chains | ||
| llm_call = llm_calls[0] | ||
| output_payload = copy.deepcopy(llm_call.content) | ||
| if ( | ||
| isinstance(output_payload, dict) | ||
| and output_payload.get("type") == "audio" | ||
| and isinstance(output_payload.get("content"), dict) | ||
| and output_payload["content"].get("format") == "uri" | ||
| ): | ||
| s3_path = output_payload["content"].get("value", "") | ||
| try: | ||
| storage = get_cloud_storage(session, project_id) | ||
| output_payload["content"]["value"] = storage.get_signed_url( | ||
| s3_path, expires_in=3600 | ||
| ) | ||
| except Exception as e: | ||
| logger.warning( | ||
| f"[get_llm_call_status] Failed to generate presigned URL for audio: {e} | job_id={job_id}" | ||
| ) | ||
| output_payload["content"]["value"] = "" | ||
| output_payload["content"]["format"] = "url" | ||
| raw_content = copy.deepcopy(llm_call.content) | ||
| output = ( | ||
| _resolve_llm_output(raw_content, project_id, session, job_id) | ||
| if isinstance(raw_content, dict) | ||
| else None | ||
| ) | ||
|
|
||
| llm_response = LLMResponse( | ||
| provider_response_id=llm_call.provider_response_id or "", | ||
| conversation_id=llm_call.conversation_id, | ||
| provider=llm_call.provider, | ||
| model=llm_call.model, | ||
| output=output_payload, | ||
| output=output, | ||
| ) | ||
|
|
||
| usage_payload = llm_call.usage | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -63,6 +63,38 @@ class Modality(StrEnum): | |||||||||||||
| FILES = "FILES" | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| # BCP-47 language codes accepted by the speech-to-speech endpoint (STT input / | ||||||||||||||
| # TTS output). Single source of truth: `SUPPORTED_LANGUAGE_CODES` in | ||||||||||||||
| # `app/services/llm/chain/utils.py` derives from this via `get_args`. | ||||||||||||||
|
Comment on lines
+66
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Make the alias the documented source of truth. This comment calls Suggested wording-# TTS output). Single source of truth: `SUPPORTED_LANGUAGE_CODES` in
-# `app/services/llm/chain/utils.py` derives from this via `get_args`.
+# TTS output). This Literal is the single source of truth; `SUPPORTED_LANGUAGE_CODES`
+# in `app/services/llm/chain/utils.py` is derived from it via `get_args`.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| STSLanguageCode = Literal[ | ||||||||||||||
| "auto", | ||||||||||||||
| "unknown", | ||||||||||||||
| "en-IN", | ||||||||||||||
| "hi-IN", | ||||||||||||||
| "bn-IN", | ||||||||||||||
| "kn-IN", | ||||||||||||||
| "ml-IN", | ||||||||||||||
| "mr-IN", | ||||||||||||||
| "od-IN", | ||||||||||||||
| "pa-IN", | ||||||||||||||
| "ta-IN", | ||||||||||||||
| "te-IN", | ||||||||||||||
| "gu-IN", | ||||||||||||||
| "as-IN", | ||||||||||||||
| "ur-IN", | ||||||||||||||
| "ne-IN", | ||||||||||||||
| "kok-IN", | ||||||||||||||
| "ks-IN", | ||||||||||||||
| "sd-IN", | ||||||||||||||
| "sa-IN", | ||||||||||||||
| "sat-IN", | ||||||||||||||
| "mni-IN", | ||||||||||||||
| "brx-IN", | ||||||||||||||
| "mai-IN", | ||||||||||||||
| "doi-IN", | ||||||||||||||
| ] | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| DEFAULT_STT_MODEL = "gemini-2.5-pro" | ||||||||||||||
| DEFAULT_TTS_MODEL = "gemini-3.1-flash-tts-preview" | ||||||||||||||
| DEFAULT_TTS_VOICE = "Kore" | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Narrow the helper contract.
dictis unconstrained, and this function never returnsNone: validation either returnsLLMOutputor raises. As per coding guidelines, every parameter and return value needs a narrow type.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Coding guidelines