cleaned up function calling frames (#43)

This commit is contained in:
chadbailey59
2024-03-08 10:13:28 -06:00
committed by GitHub
parent ce9c6ede66
commit 0db8a51b27
4 changed files with 29 additions and 24 deletions

View File

@@ -74,6 +74,9 @@ class UserStoppedSpeakingFrame(Frame):
pass pass
@dataclass() @dataclass()
class LLMFunctionStartFrame(Frame):
function_name: str
@dataclass()
class LLMFunctionCallFrame(Frame): class LLMFunctionCallFrame(Frame):
function_name: str function_name: str
arguments: str arguments: str

View File

@@ -12,11 +12,11 @@ from dailyai.pipeline.frames import (
LLMMessagesQueueFrame, LLMMessagesQueueFrame,
LLMResponseEndFrame, LLMResponseEndFrame,
LLMResponseStartFrame, LLMResponseStartFrame,
LLMFunctionStartFrame,
LLMFunctionCallFrame, LLMFunctionCallFrame,
Frame, Frame,
TextFrame, TextFrame,
TranscriptionQueueFrame, TranscriptionQueueFrame
UserStoppedSpeakingFrame
) )
from abc import abstractmethod from abc import abstractmethod
@@ -87,17 +87,24 @@ class LLMService(AIService):
if isinstance(frame, LLMMessagesQueueFrame): if isinstance(frame, LLMMessagesQueueFrame):
yield LLMResponseStartFrame() yield LLMResponseStartFrame()
async for text_chunk in self.run_llm_async(frame.messages, tool_choice): async for text_chunk in self.run_llm_async(frame.messages, tool_choice):
# We're streaming the LLM response and returning individual TextFrames for each chunk because
# we want to enable quick TTS. But if the LLM response is a function call, we don't need to yield
# each chunk because the function call is only useful as a single frame. Instead, we'll emit a
# LLMFunctionStartFrame to let downstream services know a function call is coming, then we'll
# collect the function arguments and return the entire call in a single LLMFunctionCallFrame.
if isinstance(text_chunk, str): if isinstance(text_chunk, str):
yield TextFrame(text_chunk) yield TextFrame(text_chunk)
elif text_chunk.function: elif text_chunk.function:
if text_chunk.function.name: if text_chunk.function.name:
# function_name += text_chunk.function.name function_name += text_chunk.function.name
yield LLMFunctionCallFrame(function_name=text_chunk.function.name, arguments=None) yield LLMFunctionStartFrame(function_name=text_chunk.function.name)
if text_chunk.function.arguments: if text_chunk.function.arguments:
# arguments += text_chunk.function.arguments # Keep iterating through the response to collect all the argument fragments and
yield LLMFunctionCallFrame(function_name=None, arguments=text_chunk.function.arguments) # yield a complete LLMFunctionCallFrame after run_llm_async completes
arguments += text_chunk.function.arguments
if (function_name and arguments): if (function_name and arguments):
yield LLMFunctionCallFrame(function_name=function_name, arguments=arguments)
function_name = "" function_name = ""
arguments = "" arguments = ""
yield LLMResponseEndFrame() yield LLMResponseEndFrame()

View File

@@ -328,7 +328,7 @@ class BaseTransportService():
break break
def interrupt(self): def interrupt(self):
self._logger.debug("!!! Interrupting") self._logger.debug("### Interrupting")
self._is_interrupted.set() self._is_interrupted.set()
async def get_receive_frames(self) -> AsyncGenerator[Frame, None]: async def get_receive_frames(self) -> AsyncGenerator[Frame, None]:

View File

@@ -16,11 +16,11 @@ from dailyai.services.deepgram_ai_services import DeepgramTTSService
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
from dailyai.pipeline.aggregators import LLMAssistantContextAggregator, LLMContextAggregator, LLMUserContextAggregator, UserResponseAggregator, LLMResponseAggregator from dailyai.pipeline.aggregators import LLMAssistantContextAggregator, LLMContextAggregator, LLMUserContextAggregator, UserResponseAggregator, LLMResponseAggregator
from support.runner import configure from support.runner import configure
from dailyai.pipeline.frames import LLMMessagesQueueFrame, TranscriptionQueueFrame, Frame, TextFrame, LLMFunctionCallFrame, LLMResponseEndFrame, StartFrame, AudioFrame, SpriteFrame, ImageFrame from dailyai.pipeline.frames import LLMMessagesQueueFrame, TranscriptionQueueFrame, Frame, TextFrame, LLMFunctionCallFrame, LLMFunctionStartFrame, LLMResponseEndFrame, StartFrame, AudioFrame, SpriteFrame, ImageFrame
from dailyai.services.ai_services import FrameLogger, AIService from dailyai.services.ai_services import FrameLogger, AIService
import logging import logging
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.INFO)
sounds = {} sounds = {}
sound_files = [ sound_files = [
@@ -207,8 +207,6 @@ class ChecklistProcessor(AIService):
self._messages = messages self._messages = messages
self._llm = llm self._llm = llm
self._tools = tools self._tools = tools
self._function_name = ""
self._arguments = ""
self._id = "You are Jessica, an agent for a company called Tri-County Health Services. Your job is to collect important information from the user before their doctor visit. You're talking to Chad Bailey. You should address the user by their first name and be polite and professional. You're not a medical professional, so you shouldn't provide any advice. Keep your responses short. Your job is to collect information to give to a doctor. Don't make assumptions about what values to plug into functions. Ask for clarification if a user response is ambiguous." self._id = "You are Jessica, an agent for a company called Tri-County Health Services. Your job is to collect important information from the user before their doctor visit. You're talking to Chad Bailey. You should address the user by their first name and be polite and professional. You're not a medical professional, so you shouldn't provide any advice. Keep your responses short. Your job is to collect information to give to a doctor. Don't make assumptions about what values to plug into functions. Ask for clarification if a user response is ambiguous."
self._acks = ["One sec.", "Let me confirm that.", "Thanks.", "OK."] self._acks = ["One sec.", "Let me confirm that.", "Thanks.", "OK."]
@@ -250,7 +248,7 @@ class ChecklistProcessor(AIService):
# TODO-CB: forcing a global here :/ # TODO-CB: forcing a global here :/
self._tools.clear() self._tools.clear()
self._tools.extend(this_step['tools']) self._tools.extend(this_step['tools'])
if isinstance(frame, LLMFunctionCallFrame) and frame.function_name: if isinstance(frame, LLMFunctionStartFrame):
print(f"... Preparing function call: {frame.function_name}") print(f"... Preparing function call: {frame.function_name}")
self._function_name = frame.function_name self._function_name = frame.function_name
if this_step['run_async']: if this_step['run_async']:
@@ -266,22 +264,19 @@ class ChecklistProcessor(AIService):
# Insert a quick response while we run the function # Insert a quick response while we run the function
# yield AudioFrame(sounds["clack-short-quiet.wav"]) # yield AudioFrame(sounds["clack-short-quiet.wav"])
pass pass
elif isinstance(frame, LLMFunctionCallFrame) and frame.arguments: elif isinstance(frame, LLMFunctionCallFrame):
self._arguments += frame.arguments
elif isinstance(frame, LLMResponseEndFrame):
if self._function_name and self._arguments: if frame.function_name and frame.arguments:
print( print(
f"--> Calling function: {self._function_name} with arguments:") f"--> Calling function: {frame.function_name} with arguments:")
pretty_json = re.sub("\n", "\n ", json.dumps( pretty_json = re.sub("\n", "\n ", json.dumps(
json.loads(self._arguments), indent=2)) json.loads(frame.arguments), indent=2))
print(f"--> {pretty_json}\n") print(f"--> {pretty_json}\n")
if not self._function_name in self._functions: if not frame.function_name in self._functions:
raise Exception(f"The LLM tried to call a function named {self._function_name}, which isn't in the list of known functions. Please check your prompt and/or self._functions.") raise Exception(f"The LLM tried to call a function named {frame.function_name}, which isn't in the list of known functions. Please check your prompt and/or self._functions.")
fn = getattr(self, self._function_name) fn = getattr(self, frame.function_name)
result = fn(json.loads(self._arguments)) result = fn(json.loads(frame.arguments))
self._function_name = ""
self._arguments = ""
if not this_step['run_async']: if not this_step['run_async']:
if result: if result:
current_step += 1 current_step += 1