Skip to content

Commit dcd6024

Browse files
added language and env_conversation detection tool
1 parent b460cf2 commit dcd6024

3 files changed

Lines changed: 426 additions & 241 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""
2+
Conversation Completion Tool for Voice Agents
3+
4+
Provides LLM-callable conversation ending capabilities
5+
"""
6+
7+
from typing import Dict, Any, Callable
8+
from pipecat.services.llm_service import FunctionCallParams
9+
from pipecat.frames.frames import TTSSpeakFrame, EndTaskFrame
10+
from pipecat.processors.frame_processor import FrameDirection
11+
from call_processing.log.logger import logger
12+
13+
14+
class ConversationCompletionToolFactory:
15+
"""Factory for creating conversation completion tool with runtime context"""
16+
17+
@staticmethod
18+
def create_conversation_completion_tool(
19+
task_container: Dict[str, Any],
20+
) -> Callable:
21+
"""
22+
Create conversation completion tool function with captured context
23+
24+
Args:
25+
task_container: Dictionary containing PipelineTask (populated after task creation)
26+
Format: {'task': PipelineTask | None}
27+
28+
Returns:
29+
Async function compatible with Pipecat's function calling
30+
"""
31+
32+
async def end_conversation(params: FunctionCallParams):
33+
"""
34+
LLM-callable function to end the conversation gracefully
35+
36+
This function is called by the LLM when it determines the user
37+
wants to end the conversation. It sends a farewell message and
38+
terminates the pipeline.
39+
40+
Parameters (from LLM):
41+
farewell_message: str - Optional custom farewell message
42+
(defaults to standard goodbye)
43+
"""
44+
try:
45+
# Get task from container
46+
task = task_container.get('task')
47+
if not task:
48+
error_msg = (
49+
'Pipeline task not initialized in conversation completion tool'
50+
)
51+
logger.error(error_msg)
52+
await params.result_callback({'success': False, 'error': error_msg})
53+
return
54+
55+
# Extract parameters
56+
arguments = params.arguments
57+
farewell_message = arguments.get(
58+
'farewell_message', 'Thank you for using our service! Goodbye!'
59+
)
60+
61+
logger.info(
62+
f'Conversation completion tool called - Farewell: "{farewell_message}"'
63+
)
64+
65+
# Send farewell message via TTS
66+
await params.llm.push_frame(TTSSpeakFrame(farewell_message))
67+
68+
# End the conversation
69+
await params.llm.push_frame(EndTaskFrame(), FrameDirection.UPSTREAM)
70+
71+
logger.info('Conversation ended by LLM decision')
72+
73+
# Return success result
74+
await params.result_callback(
75+
{
76+
'success': True,
77+
'status': 'complete',
78+
'farewell_sent': True,
79+
'farewell_message': farewell_message,
80+
}
81+
)
82+
83+
except Exception as e:
84+
error_msg = f'Error ending conversation: {str(e)}'
85+
logger.error(error_msg, exc_info=True)
86+
await params.result_callback({'success': False, 'error': error_msg})
87+
88+
return end_conversation
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
"""
2+
Language Detection Tool for Multi-Language Voice Agents
3+
4+
Provides LLM-callable language detection and switching capabilities
5+
"""
6+
7+
from typing import Dict, Any, List, Callable
8+
from pipecat.services.llm_service import FunctionCallParams
9+
from pipecat.frames.frames import ManuallySwitchServiceFrame, LLMMessagesUpdateFrame
10+
from call_processing.log.logger import logger
11+
from call_processing.constants.language_config import LANGUAGE_INSTRUCTIONS
12+
13+
14+
class LanguageDetectionToolFactory:
15+
"""Factory for creating language detection tool with runtime context"""
16+
17+
@staticmethod
18+
def create_language_detection_tool(
19+
task_container: Dict[str, Any],
20+
stt_services: Dict[str, Any],
21+
tts_services: Dict[str, Any],
22+
context_container: Dict[str, Any],
23+
supported_languages: List[str],
24+
default_language: str,
25+
language_state: Dict[str, Any],
26+
) -> Callable:
27+
"""
28+
Create language detection tool function with captured context
29+
30+
Args:
31+
task_container: Dictionary containing PipelineTask (populated after task creation)
32+
Format: {'task': PipelineTask | None}
33+
stt_services: Dictionary mapping language codes to STT services
34+
tts_services: Dictionary mapping language codes to TTS services
35+
context_container: Dictionary containing LLMContext (populated after context creation)
36+
Format: {'context': LLMContext | None}
37+
supported_languages: List of supported language codes
38+
default_language: Default language code
39+
language_state: Dictionary to track current language and switch count
40+
Format: {'current_language': str, 'switch_count': int, 'original_system_prompt': str}
41+
42+
Returns:
43+
Async function compatible with Pipecat's function calling
44+
"""
45+
46+
async def detect_and_switch_language(params: FunctionCallParams):
47+
"""
48+
LLM-callable function to detect and switch conversation language
49+
50+
This function is called by the LLM when it determines the user
51+
wants to switch to a different language. It validates the request,
52+
performs the service switch, and updates the system prompt.
53+
54+
Parameters (from LLM):
55+
target_language: str - Language code to switch to (e.g., 'es', 'hi', 'en')
56+
user_intent: str - User's stated language preference (for logging)
57+
"""
58+
try:
59+
# Get task and context from containers
60+
task = task_container.get('task')
61+
if not task:
62+
error_msg = (
63+
'Pipeline task not initialized in language detection tool'
64+
)
65+
logger.error(error_msg)
66+
await params.result_callback({'success': False, 'error': error_msg})
67+
return
68+
69+
context = context_container.get('context')
70+
if not context:
71+
error_msg = 'LLM context not initialized in language detection tool'
72+
logger.error(error_msg)
73+
await params.result_callback({'success': False, 'error': error_msg})
74+
return
75+
76+
# Extract parameters
77+
arguments = params.arguments
78+
target_language = arguments.get('target_language', '').lower()
79+
user_intent = arguments.get('user_intent', 'Unknown')
80+
81+
current_language = language_state.get(
82+
'current_language', default_language
83+
)
84+
switch_count = language_state.get('switch_count', 0)
85+
86+
logger.info(
87+
f'Language detection tool called - Target: {target_language}, '
88+
f'Current: {current_language}, User intent: {user_intent}'
89+
)
90+
91+
# Validation 1: Check if target language is supported
92+
if target_language not in supported_languages:
93+
error_msg = (
94+
f"Language '{target_language}' is not supported. "
95+
f"Supported languages: {', '.join(supported_languages)}"
96+
)
97+
logger.warning(error_msg)
98+
await params.result_callback(
99+
{
100+
'success': False,
101+
'error': error_msg,
102+
'current_language': current_language,
103+
'supported_languages': supported_languages,
104+
}
105+
)
106+
return
107+
108+
# Validation 2: Check if already in target language
109+
if target_language == current_language:
110+
logger.info(f'Already using language: {target_language}')
111+
await params.result_callback(
112+
{
113+
'success': True,
114+
'message': f'Already using {target_language}',
115+
'current_language': current_language,
116+
'switch_performed': False,
117+
}
118+
)
119+
return
120+
121+
# Validation 3: Check if services exist for target language
122+
if (
123+
target_language not in stt_services
124+
or target_language not in tts_services
125+
):
126+
error_msg = (
127+
f'Services not configured for language: {target_language}'
128+
)
129+
logger.error(error_msg)
130+
await params.result_callback(
131+
{
132+
'success': False,
133+
'error': error_msg,
134+
'current_language': current_language,
135+
}
136+
)
137+
return
138+
139+
# Perform language switch
140+
try:
141+
target_stt = stt_services[target_language]
142+
target_tts = tts_services[target_language]
143+
144+
# Queue service switch frames
145+
await task.queue_frames(
146+
[
147+
ManuallySwitchServiceFrame(service=target_stt),
148+
ManuallySwitchServiceFrame(service=target_tts),
149+
]
150+
)
151+
152+
logger.info(
153+
f'Switched STT/TTS services from {current_language} to {target_language}'
154+
)
155+
156+
# Update system prompt with language instruction
157+
language_instruction = LANGUAGE_INSTRUCTIONS.get(
158+
target_language,
159+
LANGUAGE_INSTRUCTIONS.get('en', 'Respond in English.'),
160+
)
161+
162+
# Get base prompt without language instruction (must exist for multi-language)
163+
base_prompt = language_state.get('original_system_prompt')
164+
if not base_prompt:
165+
error_msg = 'Original system prompt not found in language state'
166+
logger.error(error_msg)
167+
await params.result_callback(
168+
{'success': False, 'error': error_msg}
169+
)
170+
return
171+
172+
# Append new language instruction to clean base prompt
173+
updated_content = f'{base_prompt}\n\n{language_instruction}'
174+
updated_system_message = {
175+
'role': 'system',
176+
'content': updated_content,
177+
}
178+
179+
# Update context
180+
current_messages = context.get_messages()
181+
new_messages = [updated_system_message] + current_messages[1:]
182+
await task.queue_frame(
183+
LLMMessagesUpdateFrame(new_messages, run_llm=False)
184+
)
185+
186+
logger.info(
187+
f'Updated system prompt with {target_language} instruction'
188+
)
189+
190+
# Update state
191+
language_state['current_language'] = target_language
192+
language_state['switch_count'] = switch_count + 1
193+
194+
# Return success result
195+
await params.result_callback(
196+
{
197+
'success': True,
198+
'message': f'Language switched to {target_language}',
199+
'previous_language': current_language,
200+
'current_language': target_language,
201+
'switch_performed': True,
202+
'switch_count': language_state['switch_count'],
203+
}
204+
)
205+
206+
except Exception as e:
207+
error_msg = f'Error switching services: {str(e)}'
208+
logger.error(error_msg, exc_info=True)
209+
await params.result_callback(
210+
{
211+
'success': False,
212+
'error': error_msg,
213+
'current_language': current_language,
214+
}
215+
)
216+
217+
except Exception as e:
218+
error_msg = f'Error in language detection tool: {str(e)}'
219+
logger.error(error_msg, exc_info=True)
220+
await params.result_callback({'success': False, 'error': error_msg})
221+
222+
return detect_and_switch_language

0 commit comments

Comments
 (0)