Skip to content

Commit 0a2e567

Browse files
language and endconversation detection tool (#210)
* added language and env_conversation detection tool * lang detection -> parallel pipeline instead of service switcher * little prompt fix for tool
1 parent 8d35f5a commit 0a2e567

3 files changed

Lines changed: 534 additions & 276 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: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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 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+
language_switcher: Any,
21+
stt_language_switcher: 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+
language_switcher: LanguageSwitcher instance that manages TTS routing
34+
stt_language_switcher: STTLanguageSwitcher instance that manages STT routing
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+
# Perform language switch
122+
try:
123+
# Update TTS language switcher state
124+
language_switcher.set_language(target_language)
125+
126+
# Update STT language switcher state
127+
stt_language_switcher.set_language(target_language)
128+
129+
logger.info(
130+
f'Switched TTS and STT language from {current_language} to {target_language}'
131+
)
132+
133+
# Update system prompt with language instruction
134+
language_instruction = LANGUAGE_INSTRUCTIONS.get(
135+
target_language,
136+
LANGUAGE_INSTRUCTIONS.get('en', 'Respond in English.'),
137+
)
138+
139+
# Get base prompt without language instruction (must exist for multi-language)
140+
base_prompt = language_state.get('original_system_prompt')
141+
if not base_prompt:
142+
error_msg = 'Original system prompt not found in language state'
143+
logger.error(error_msg)
144+
await params.result_callback(
145+
{'success': False, 'error': error_msg}
146+
)
147+
return
148+
149+
# Append new language instruction to clean base prompt
150+
updated_content = f'{base_prompt}\n\n{language_instruction}'
151+
updated_system_message = {
152+
'role': 'system',
153+
'content': updated_content,
154+
}
155+
156+
# Update context
157+
current_messages = context.get_messages()
158+
new_messages = [updated_system_message] + current_messages[1:]
159+
await task.queue_frame(
160+
LLMMessagesUpdateFrame(new_messages, run_llm=False)
161+
)
162+
163+
logger.info(
164+
f'Updated system prompt with {target_language} instruction'
165+
)
166+
167+
# Update state
168+
language_state['current_language'] = target_language
169+
language_state['switch_count'] = switch_count + 1
170+
171+
# Return success result
172+
await params.result_callback(
173+
{
174+
'success': True,
175+
'message': f'Language switched to {target_language}',
176+
'previous_language': current_language,
177+
'current_language': target_language,
178+
'switch_performed': True,
179+
'switch_count': language_state['switch_count'],
180+
}
181+
)
182+
183+
except Exception as e:
184+
error_msg = f'Error switching services: {str(e)}'
185+
logger.error(error_msg, exc_info=True)
186+
await params.result_callback(
187+
{
188+
'success': False,
189+
'error': error_msg,
190+
'current_language': current_language,
191+
}
192+
)
193+
194+
except Exception as e:
195+
error_msg = f'Error in language detection tool: {str(e)}'
196+
logger.error(error_msg, exc_info=True)
197+
await params.result_callback({'success': False, 'error': error_msg})
198+
199+
return detect_and_switch_language

0 commit comments

Comments
 (0)