feat: add support for self hosted llm models

This commit is contained in:
Abhishek Kumar 2026-03-24 17:50:45 +05:30
parent 31e075d114
commit ac0731a374
17 changed files with 179 additions and 48 deletions

1
.gitignore vendored
View file

@ -16,3 +16,4 @@ venv/
.playwright-mcp .playwright-mcp
coturn/ coturn/
*.wav *.wav
dograh_pcm_cache/

View file

@ -64,7 +64,7 @@ class WorkflowRecordingClient(BaseDBClient):
storage_key=storage_key, storage_key=storage_key,
storage_backend=storage_backend, storage_backend=storage_backend,
created_by=created_by, created_by=created_by,
metadata=metadata or {}, recording_metadata=metadata or {},
) )
session.add(recording) session.add(recording)

View file

@ -40,6 +40,7 @@ class UserConfigurationValidator:
ServiceProviders.SPEECHMATICS.value: self._check_speechmatics_api_key, ServiceProviders.SPEECHMATICS.value: self._check_speechmatics_api_key,
ServiceProviders.CAMB.value: self._check_camb_api_key, ServiceProviders.CAMB.value: self._check_camb_api_key,
ServiceProviders.AWS_BEDROCK.value: self._check_aws_bedrock_api_key, ServiceProviders.AWS_BEDROCK.value: self._check_aws_bedrock_api_key,
ServiceProviders.SELF_HOSTED.value: self._check_self_hosted_api_key,
} }
async def validate(self, configuration: UserConfiguration) -> APIKeyStatusResponse: async def validate(self, configuration: UserConfiguration) -> APIKeyStatusResponse:
@ -74,6 +75,20 @@ class UserConfigurationValidator:
provider = service_config.provider provider = service_config.provider
# Self-hosted doesn't require an API key
if provider == ServiceProviders.SELF_HOSTED.value:
try:
if not self._check_self_hosted_api_key(provider, service_config):
return [
{
"model": service_name,
"message": f"Invalid {provider} configuration",
}
]
except ValueError as e:
return [{"model": service_name, "message": str(e)}]
return []
# AWS Bedrock uses AWS credentials instead of api_key # AWS Bedrock uses AWS credentials instead of api_key
if provider == ServiceProviders.AWS_BEDROCK.value: if provider == ServiceProviders.AWS_BEDROCK.value:
try: try:
@ -163,7 +178,12 @@ class UserConfigurationValidator:
def _check_camb_api_key(self, model: str, api_key: str) -> bool: def _check_camb_api_key(self, model: str, api_key: str) -> bool:
return True return True
def _check_self_hosted_api_key(self, model: str, service_config) -> bool:
if not getattr(service_config, "base_url", None):
raise ValueError("base_url is required for self-hosted LLM")
return True
def _check_aws_bedrock_api_key(self, model: str, service_config) -> bool: def _check_aws_bedrock_api_key(self, model: str, service_config) -> bool:
if not service_config.aws_access_key or not service_config.aws_secret_key: if not service_config.aws_access_key or not service_config.aws_secret_key:
raise ValueError("AWS access key and secret key are required for Bedrock") raise ValueError("AWS access key and secret key are required for Bedrock")

View file

@ -27,6 +27,7 @@ class ServiceProviders(str, Enum):
SPEECHMATICS = "speechmatics" SPEECHMATICS = "speechmatics"
CAMB = "camb" CAMB = "camb"
AWS_BEDROCK = "aws_bedrock" AWS_BEDROCK = "aws_bedrock"
SELF_HOSTED = "self_hosted"
class BaseServiceConfiguration(BaseModel): class BaseServiceConfiguration(BaseModel):
@ -40,6 +41,7 @@ class BaseServiceConfiguration(BaseModel):
ServiceProviders.AZURE, ServiceProviders.AZURE,
ServiceProviders.DOGRAH, ServiceProviders.DOGRAH,
ServiceProviders.AWS_BEDROCK, ServiceProviders.AWS_BEDROCK,
ServiceProviders.SELF_HOSTED,
# ServiceProviders.SARVAM, # ServiceProviders.SARVAM,
] ]
api_key: str | list[str] api_key: str | list[str]
@ -249,6 +251,22 @@ class AWSBedrockLLMConfiguration(BaseLLMConfiguration):
api_key: str | list[str] | None = Field(default=None) api_key: str | list[str] | None = Field(default=None)
SELF_HOSTED_LLM_MODELS = ["llama3", "mistral", "phi3", "qwen2", "gemma2", "deepseek-r1"]
@register_llm
class SelfHostedLLMConfiguration(BaseLLMConfiguration):
provider: Literal[ServiceProviders.SELF_HOSTED] = ServiceProviders.SELF_HOSTED
model: str = Field(
default="llama3", json_schema_extra={"examples": SELF_HOSTED_LLM_MODELS}
)
base_url: str = Field(
default="http://localhost:11434/v1",
description="OpenAI-compatible endpoint (Ollama, vLLM, etc.)",
)
api_key: str | list[str] | None = Field(default=None)
LLMConfig = Annotated[ LLMConfig = Annotated[
Union[ Union[
OpenAILLMService, OpenAILLMService,
@ -258,6 +276,7 @@ LLMConfig = Annotated[
AzureLLMService, AzureLLMService,
DograhLLMService, DograhLLMService,
AWSBedrockLLMConfiguration, AWSBedrockLLMConfiguration,
SelfHostedLLMConfiguration,
], ],
Field(discriminator="provider"), Field(discriminator="provider"),
] ]
@ -334,6 +353,12 @@ class CartesiaTTSConfiguration(BaseTTSConfiguration):
) )
voice: str = Field(default="3faa81ae-d3d8-4ab1-9e44-e50e46d33c30") voice: str = Field(default="3faa81ae-d3d8-4ab1-9e44-e50e46d33c30")
speed: float = Field(default=1.0, ge=0.6, le=1.5, description="Speed of the voice") speed: float = Field(default=1.0, ge=0.6, le=1.5, description="Speed of the voice")
volume: float = Field(
default=1.0,
ge=0.5,
le=2.0,
description="Volume multiplier for generated speech",
)
SARVAM_TTS_MODELS = ["bulbul:v2", "bulbul:v3"] SARVAM_TTS_MODELS = ["bulbul:v2", "bulbul:v3"]

View file

@ -66,6 +66,7 @@ class RecordingRouterProcessor(FrameProcessor):
self._frame_buffer: list[tuple[LLMTextFrame, FrameDirection]] = [] self._frame_buffer: list[tuple[LLMTextFrame, FrameDirection]] = []
self._mode: Optional[str] = None # None = detecting, "tts", "recording" self._mode: Optional[str] = None # None = detecting, "tts", "recording"
self._recording_id_buffer = "" self._recording_id_buffer = ""
self._recording_playback_started = False
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Frame dispatch # Frame dispatch
@ -99,9 +100,15 @@ class RecordingRouterProcessor(FrameProcessor):
await self.push_frame(frame, direction) await self.push_frame(frame, direction)
return return
# --- Recording mode: accumulate recording_id silently --- # --- Recording mode: accumulate text and start playback ASAP ---
if self._mode == "recording": if self._mode == "recording":
self._recording_id_buffer += frame.text self._recording_id_buffer += frame.text
if not self._recording_playback_started:
buf = self._recording_id_buffer.lstrip()
if " " in buf:
recording_id = buf.split()[0]
self._recording_playback_started = True
await self._play_recording(recording_id)
return return
# --- Detection mode: buffer until marker found --- # --- Detection mode: buffer until marker found ---
@ -178,16 +185,21 @@ class RecordingRouterProcessor(FrameProcessor):
self, frame: LLMFullResponseEndFrame, direction: FrameDirection self, frame: LLMFullResponseEndFrame, direction: FrameDirection
): ):
if self._mode == "recording": if self._mode == "recording":
recording_id = self._recording_id_buffer.strip() full_text = self._recording_id_buffer.strip()
if recording_id: if full_text:
# Push accumulated text as TTSTextFrame for UI feedback via observer recording_id = full_text.split()[0]
# Push full text (marker + id + transcript) for assistant context
await self.push_frame( await self.push_frame(
TTSTextFrame( TTSTextFrame(
text=RECORDING_MARKER + self._recording_id_buffer, text=RECORDING_MARKER + self._recording_id_buffer,
aggregated_by="recording_router", aggregated_by="recording_router",
) )
) )
await self._play_recording(recording_id)
# Fallback: if response ended before a space arrived (no transcript)
if not self._recording_playback_started:
await self._play_recording(recording_id)
else: else:
logger.warning( logger.warning(
"RecordingRouterProcessor: recording mode but empty recording_id" "RecordingRouterProcessor: recording mode but empty recording_id"
@ -256,3 +268,4 @@ class RecordingRouterProcessor(FrameProcessor):
self._frame_buffer = [] self._frame_buffer = []
self._mode = None self._mode = None
self._recording_id_buffer = "" self._recording_id_buffer = ""
self._recording_playback_started = False

View file

@ -8,7 +8,11 @@ from api.services.configuration.registry import ServiceProviders
from pipecat.services.aws.llm import AWSBedrockLLMService, AWSBedrockLLMSettings from pipecat.services.aws.llm import AWSBedrockLLMService, AWSBedrockLLMSettings
from pipecat.services.azure.llm import AzureLLMService, AzureLLMSettings from pipecat.services.azure.llm import AzureLLMService, AzureLLMSettings
from pipecat.services.cartesia.stt import CartesiaSTTService from pipecat.services.cartesia.stt import CartesiaSTTService
from pipecat.services.cartesia.tts import CartesiaTTSService, CartesiaTTSSettings, GenerationConfig from pipecat.services.cartesia.tts import (
CartesiaTTSService,
CartesiaTTSSettings,
GenerationConfig,
)
from pipecat.services.deepgram.flux.stt import ( from pipecat.services.deepgram.flux.stt import (
DeepgramFluxSTTService, DeepgramFluxSTTService,
DeepgramFluxSTTSettings, DeepgramFluxSTTSettings,
@ -212,13 +216,19 @@ def create_tts_service(user_config, audio_config: "AudioConfig"):
) )
elif user_config.tts.provider == ServiceProviders.CARTESIA.value: elif user_config.tts.provider == ServiceProviders.CARTESIA.value:
speed = getattr(user_config.tts, "speed", None) speed = getattr(user_config.tts, "speed", None)
generation_config = GenerationConfig(speed=speed) if speed and speed != 1.0 else None generation_config = (
GenerationConfig(speed=speed) if speed and speed != 1.0 else None
)
return CartesiaTTSService( return CartesiaTTSService(
api_key=user_config.tts.api_key, api_key=user_config.tts.api_key,
settings=CartesiaTTSSettings( settings=CartesiaTTSSettings(
voice=user_config.tts.voice, voice=user_config.tts.voice,
model=user_config.tts.model, model=user_config.tts.model,
**({"generation_config": generation_config} if generation_config else {}), **(
{"generation_config": generation_config}
if generation_config
else {}
),
), ),
text_filters=[xml_function_tag_filter], text_filters=[xml_function_tag_filter],
silence_time_s=1.0, silence_time_s=1.0,
@ -353,6 +363,12 @@ def create_llm_service_from_provider(
aws_region=aws_region, aws_region=aws_region,
settings=AWSBedrockLLMSettings(model=model), settings=AWSBedrockLLMSettings(model=model),
) )
elif provider == ServiceProviders.SELF_HOSTED.value:
return OpenAILLMService(
base_url=base_url or "http://localhost:11434/v1",
api_key=api_key or "none",
settings=OpenAILLMSettings(model=model),
)
else: else:
raise HTTPException(status_code=400, detail=f"Invalid LLM provider {provider}") raise HTTPException(status_code=400, detail=f"Invalid LLM provider {provider}")
@ -368,6 +384,8 @@ def create_llm_service(user_config):
kwargs["base_url"] = user_config.llm.base_url kwargs["base_url"] = user_config.llm.base_url
elif provider == ServiceProviders.AZURE.value: elif provider == ServiceProviders.AZURE.value:
kwargs["endpoint"] = user_config.llm.endpoint kwargs["endpoint"] = user_config.llm.endpoint
elif provider == ServiceProviders.SELF_HOSTED.value:
kwargs["base_url"] = user_config.llm.base_url
elif provider == ServiceProviders.AWS_BEDROCK.value: elif provider == ServiceProviders.AWS_BEDROCK.value:
kwargs["aws_access_key"] = user_config.llm.aws_access_key kwargs["aws_access_key"] = user_config.llm.aws_access_key
kwargs["aws_secret_key"] = user_config.llm.aws_secret_key kwargs["aws_secret_key"] = user_config.llm.aws_secret_key

View file

@ -437,9 +437,7 @@ class PipecatEngine:
async def _do_extraction(): async def _do_extraction():
try: try:
logger.debug( logger.debug(f"Starting variable extraction for node: {node.name}")
f"Starting variable extraction for node: {node.name}"
)
extracted_data = ( extracted_data = (
await self._variable_extraction_manager._perform_extraction( await self._variable_extraction_manager._perform_extraction(
extraction_variables, parent_context, extraction_prompt extraction_variables, parent_context, extraction_prompt
@ -454,7 +452,9 @@ class PipecatEngine:
f"Variable extraction completed for node: {node.name}. Extracted: {extracted_data}" f"Variable extraction completed for node: {node.name}. Extracted: {extracted_data}"
) )
except Exception as e: except Exception as e:
logger.error(f"Error during variable extraction for node {node.name}: {str(e)}") logger.error(
f"Error during variable extraction for node {node.name}: {str(e)}"
)
if run_in_background: if run_in_background:
logger.debug( logger.debug(
@ -497,9 +497,7 @@ class PipecatEngine:
logger.error( logger.error(
f"Pending extraction task '{task_name}' failed: {result}" f"Pending extraction task '{task_name}' failed: {result}"
) )
logger.debug( logger.debug(f"All pending extraction tasks completed in {elapsed:.2f}s")
f"All pending extraction tasks completed in {elapsed:.2f}s"
)
except asyncio.TimeoutError: except asyncio.TimeoutError:
incomplete = [ incomplete = [
t.get_name() for t in self._pending_extraction_tasks if not t.done() t.get_name() for t in self._pending_extraction_tasks if not t.done()

View file

@ -34,13 +34,13 @@ You have two modes for responding:
Example: Hello! How can I help you today? Example: Hello! How can I help you today?
2. PRE-RECORDED AUDIO (): Play a pre-recorded audio message. 2. PRE-RECORDED AUDIO (): Play a pre-recorded audio message.
Format: `` followed by a space and ONLY the recording_id. Nothing else. Format: `` followed by a space followed by recording_id followed by provided transcript. Nothing else.
Example: rec_greeting_01 Example: rec_greeting_01 [ Provided Transcript ]
RULES: RULES:
- Your response MUST start with either `` or `` as the very first character. - Your response MUST start with either `` or `` as the very first character.
- For `` (dynamic speech): Follow with a space and your full response text. - For `` (dynamic speech): Follow with a space and your full response text.
- For `` (pre-recorded audio): Follow with a space and ONLY the recording_id. No other text. - For `` (pre-recorded audio): Follow with a space and the recording_id and the provided transcript. No other text.
- Use `` when a pre-recorded message matches the situation well. - Use `` when a pre-recorded message matches the situation well.
- Use `` when you need to generate a dynamic, contextual response. - Use `` when you need to generate a dynamic, contextual response.
- NEVER mix modes in a single response. Choose one.""" - NEVER mix modes in a single response. Choose one."""

View file

@ -28,7 +28,9 @@ from api.utils.template_renderer import render_template
from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.aggregators.llm_context import LLMContext
async def _run_llm_inference(llm, messages: list[dict], system_prompt: str) -> str | None: async def _run_llm_inference(
llm, messages: list[dict], system_prompt: str
) -> str | None:
"""Run a one-shot LLM inference using the pipecat service.""" """Run a one-shot LLM inference using the pipecat service."""
context = LLMContext() context = LLMContext()
context.set_messages(messages) context.set_messages(messages)
@ -51,7 +53,10 @@ async def _generate_conversation_summary(
] ]
try: try:
summary = await _run_llm_inference(llm, messages, CONVERSATION_SUMMARY_SYSTEM_PROMPT) or "" summary = (
await _run_llm_inference(llm, messages, CONVERSATION_SUMMARY_SYSTEM_PROMPT)
or ""
)
span_name = f"conversation-summary-before-{node_name}" span_name = f"conversation-summary-before-{node_name}"
add_qa_span_to_trace(parent_ctx, model, messages, summary, span_name) add_qa_span_to_trace(parent_ctx, model, messages, summary, span_name)

View file

@ -154,7 +154,12 @@ async def ensure_node_summaries(
try: try:
context = LLMContext() context = LLMContext()
context.set_messages(messages) context.set_messages(messages)
summary_text = await llm.run_inference(context, system_instruction=NODE_SUMMARY_SYSTEM_PROMPT) or "" summary_text = (
await llm.run_inference(
context, system_instruction=NODE_SUMMARY_SYSTEM_PROMPT
)
or ""
)
except Exception as e: except Exception as e:
logger.warning(f"Failed to generate summary for node {node_id}: {e}") logger.warning(f"Failed to generate summary for node {node_id}: {e}")
updated_summaries[node_id] = {"summary": ""} updated_summaries[node_id] = {"summary": ""}

View file

@ -9,7 +9,7 @@ Covers:
""" """
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import MagicMock, patch
import pytest import pytest
from pydantic import ValidationError from pydantic import ValidationError
@ -17,13 +17,12 @@ from pydantic import ValidationError
from api.services.configuration.check_validity import UserConfigurationValidator from api.services.configuration.check_validity import UserConfigurationValidator
from api.services.configuration.registry import ( from api.services.configuration.registry import (
CAMB_TTS_MODELS, CAMB_TTS_MODELS,
CambTTSConfiguration,
REGISTRY, REGISTRY,
CambTTSConfiguration,
ServiceProviders, ServiceProviders,
ServiceType, ServiceType,
) )
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 1. CambTTSConfiguration model tests # 1. CambTTSConfiguration model tests
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View file

@ -48,6 +48,12 @@ new api route in backend, and wish to use it in the UI, generate the client usin
npm run generate-client npm run generate-client
``` ```
## Conventions
### File Uploads
Always use a hidden `<input type="file">` with a visible `<Button>` that triggers it via `fileInputRef.current?.click()`. Never use a visible `<Input type="file">` — the native file input styling is inconsistent and confusing. Show the selected filename next to or below the button.
## Development ## Development
```bash ```bash

View file

@ -519,13 +519,17 @@ export default function RunsPage() {
variant="outline" variant="outline"
size="icon" size="icon"
onClick={() => { onClick={() => {
const filter = encodeURIComponent( if (run.gathered_context?.trace_url) {
`metadata;stringObject;attributes;contains;conversation.id,metadata;stringObject;attributes;contains;${run.id}`, window.open(String(run.gathered_context.trace_url), '_blank');
); } else {
window.open( const filter = encodeURIComponent(
`${process.env.NEXT_PUBLIC_LANGFUSE_ENDPOINT}/project/${process.env.NEXT_PUBLIC_LANGFUSE_PROJECT_ID}/traces?search=&filter=${filter}&dateRange=All+time`, `metadata;stringObject;attributes;contains;conversation.id,metadata;stringObject;attributes;contains;${run.id}`,
'_blank', );
); window.open(
`${process.env.NEXT_PUBLIC_LANGFUSE_ENDPOINT}/project/${process.env.NEXT_PUBLIC_LANGFUSE_PROJECT_ID}/traces?search=&filter=${filter}&dateRange=All+time`,
'_blank',
);
}
}} }}
> >
<Image <Image

View file

@ -216,7 +216,7 @@ export const RecordingsDialog = ({
<Label className="text-xs text-muted-foreground"> <Label className="text-xs text-muted-foreground">
Audio File Audio File
</Label> </Label>
<Input <input
ref={fileInputRef} ref={fileInputRef}
type="file" type="file"
accept="audio/*" accept="audio/*"
@ -233,11 +233,24 @@ export const RecordingsDialog = ({
setError(null); setError(null);
setSelectedFile(file); setSelectedFile(file);
}} }}
className="text-sm" className="hidden"
/> />
<p className="text-xs text-muted-foreground mt-1"> <Button
Max 5MB type="button"
</p> variant="outline"
size="sm"
className="w-full justify-start text-sm font-normal"
onClick={() => fileInputRef.current?.click()}
>
<Upload className="w-4 h-4 mr-2 shrink-0" />
{selectedFile ? (
<span className="truncate">
{selectedFile.name} ({(selectedFile.size / (1024 * 1024)).toFixed(1)}MB)
</span>
) : (
<span className="text-muted-foreground">Choose audio file (max 5MB)</span>
)}
</Button>
</div> </div>
<div> <div>
<Label className="text-xs text-muted-foreground"> <Label className="text-xs text-muted-foreground">
@ -289,8 +302,8 @@ export const RecordingsDialog = ({
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<code className="text-xs bg-muted px-1.5 py-0.5 rounded font-mono"> <code className="text-xs bg-muted px-1.5 py-0.5 rounded font-mono truncate max-w-[300px]">
{rec.recording_id} {(rec.metadata?.original_filename as string) || rec.recording_id}
</code> </code>
</div> </div>
<p className="text-sm text-muted-foreground mt-1 break-all line-clamp-2"> <p className="text-sm text-muted-foreground mt-1 break-all line-clamp-2">

View file

@ -18,14 +18,12 @@ export const layoutNodes = (
// Separate nodes by type // Separate nodes by type
const triggerNodes = nodes.filter(n => n.type === NodeType.TRIGGER); const triggerNodes = nodes.filter(n => n.type === NodeType.TRIGGER);
const webhookNodes = nodes.filter(n => n.type === NodeType.WEBHOOK); const webhookNodes = nodes.filter(n => n.type === NodeType.WEBHOOK);
const globalNodes = nodes.filter(n => n.type === NodeType.GLOBAL_NODE || n.type === 'global'); const qaNodes = nodes.filter(n => n.type === NodeType.QA);
const globalNodes = nodes.filter(n => n.type === NodeType.GLOBAL_NODE);
const workflowNodes = nodes.filter(n => const workflowNodes = nodes.filter(n =>
n.type === NodeType.START_CALL || n.type === NodeType.START_CALL ||
n.type === NodeType.AGENT_NODE || n.type === NodeType.AGENT_NODE ||
n.type === NodeType.END_CALL || n.type === NodeType.END_CALL
n.type === 'startCall' ||
n.type === 'agentNode' ||
n.type === 'endCall'
); );
// If no workflow nodes, just return original nodes // If no workflow nodes, just return original nodes
@ -161,12 +159,26 @@ export const layoutNodes = (
}; };
}); });
// Position QA nodes below webhook nodes on the right side
const qaStartY = webhookNodes.length > 0
? workflowCenterY - (webhookNodes.length * NODE_HEIGHT + (webhookNodes.length - 1) * VERTICAL_SPACING) / 2
+ webhookNodes.length * (NODE_HEIGHT + VERTICAL_SPACING) + VERTICAL_SPACING
: workflowCenterY;
const positionedQaNodes = qaNodes.map((node, index) => ({
...node,
position: {
x: webhookNodesX,
y: qaStartY + index * (NODE_HEIGHT + VERTICAL_SPACING)
}
}));
// Combine all positioned nodes // Combine all positioned nodes
const allPositionedNodes = [ const allPositionedNodes = [
...positionedTriggerNodes, ...positionedTriggerNodes,
...positionedGlobalNodes, ...positionedGlobalNodes,
...positionedWorkflowNodes, ...positionedWorkflowNodes,
...positionedWebhookNodes ...positionedWebhookNodes,
...positionedQaNodes
]; ];
// Create a map for quick lookup // Create a map for quick lookup

View file

@ -236,11 +236,21 @@ export default function ServiceConfiguration() {
} }
}); });
selectedProviders[service] = userConfig?.[service]?.provider as string; selectedProviders[service] = userConfig?.[service]?.provider as string;
// Fill in schema defaults for fields not present in userConfig
const properties = response.data[service]?.[selectedProviders[service]]?.properties as Record<string, SchemaProperty>;
if (properties) {
Object.entries(properties).forEach(([field, schema]) => {
const key = `${service}_${field}`;
if (field !== "provider" && field !== "api_key" && schema.default !== undefined && !(key in defaultValues)) {
defaultValues[key] = schema.default;
}
});
}
} else { } else {
const properties = response.data[service]?.[selectedProviders[service]]?.properties as Record<string, SchemaProperty>; const properties = response.data[service]?.[selectedProviders[service]]?.properties as Record<string, SchemaProperty>;
if (properties) { if (properties) {
Object.entries(properties).forEach(([field, schema]) => { Object.entries(properties).forEach(([field, schema]) => {
if (field !== "provider" && schema.default) { if (field !== "provider" && schema.default !== undefined) {
defaultValues[`${service}_${field}`] = schema.default; defaultValues[`${service}_${field}`] = schema.default;
} }
}); });

View file

@ -15,6 +15,7 @@ export interface MentionItem {
id: string; id: string;
name: string; name: string;
description: string; description: string;
filename: string;
} }
interface MentionTextareaProps { interface MentionTextareaProps {
@ -46,6 +47,7 @@ export function MentionTextarea({
id: r.recording_id, id: r.recording_id,
name: r.transcript, name: r.transcript,
description: r.transcript, description: r.transcript,
filename: (r.metadata?.original_filename as string) || r.recording_id,
})), })),
[recordings] [recordings]
); );
@ -195,7 +197,7 @@ export function MentionTextarea({
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<code className="text-xs bg-muted px-1 py-0.5 rounded font-mono"> <code className="text-xs bg-muted px-1 py-0.5 rounded font-mono">
{item.id} {item.filename}
</code> </code>
<span className="font-medium truncate">{item.name}</span> <span className="font-medium truncate">{item.name}</span>
</div> </div>