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
@dataclass()
class LLMFunctionStartFrame(Frame):
function_name: str
@dataclass()
class LLMFunctionCallFrame(Frame):
function_name: str
arguments: str

View File

@@ -12,11 +12,11 @@ from dailyai.pipeline.frames import (
LLMMessagesQueueFrame,
LLMResponseEndFrame,
LLMResponseStartFrame,
LLMFunctionStartFrame,
LLMFunctionCallFrame,
Frame,
TextFrame,
TranscriptionQueueFrame,
UserStoppedSpeakingFrame
TranscriptionQueueFrame
)
from abc import abstractmethod
@@ -87,17 +87,24 @@ class LLMService(AIService):
if isinstance(frame, LLMMessagesQueueFrame):
yield LLMResponseStartFrame()
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):
yield TextFrame(text_chunk)
elif text_chunk.function:
if text_chunk.function.name:
# function_name += text_chunk.function.name
yield LLMFunctionCallFrame(function_name=text_chunk.function.name, arguments=None)
function_name += text_chunk.function.name
yield LLMFunctionStartFrame(function_name=text_chunk.function.name)
if text_chunk.function.arguments:
# arguments += text_chunk.function.arguments
yield LLMFunctionCallFrame(function_name=None, arguments=text_chunk.function.arguments)
# Keep iterating through the response to collect all the argument fragments and
# yield a complete LLMFunctionCallFrame after run_llm_async completes
arguments += text_chunk.function.arguments
if (function_name and arguments):
yield LLMFunctionCallFrame(function_name=function_name, arguments=arguments)
function_name = ""
arguments = ""
yield LLMResponseEndFrame()

View File

@@ -328,7 +328,7 @@ class BaseTransportService():
break
def interrupt(self):
self._logger.debug("!!! Interrupting")
self._logger.debug("### Interrupting")
self._is_interrupted.set()
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.pipeline.aggregators import LLMAssistantContextAggregator, LLMContextAggregator, LLMUserContextAggregator, UserResponseAggregator, LLMResponseAggregator
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
import logging
logging.basicConfig(level=logging.DEBUG)
logging.basicConfig(level=logging.INFO)
sounds = {}
sound_files = [
@@ -207,8 +207,6 @@ class ChecklistProcessor(AIService):
self._messages = messages
self._llm = llm
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._acks = ["One sec.", "Let me confirm that.", "Thanks.", "OK."]
@@ -250,7 +248,7 @@ class ChecklistProcessor(AIService):
# TODO-CB: forcing a global here :/
self._tools.clear()
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}")
self._function_name = frame.function_name
if this_step['run_async']:
@@ -266,22 +264,19 @@ class ChecklistProcessor(AIService):
# Insert a quick response while we run the function
# yield AudioFrame(sounds["clack-short-quiet.wav"])
pass
elif isinstance(frame, LLMFunctionCallFrame) and frame.arguments:
self._arguments += frame.arguments
elif isinstance(frame, LLMResponseEndFrame):
elif isinstance(frame, LLMFunctionCallFrame):
if self._function_name and self._arguments:
if frame.function_name and frame.arguments:
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(
json.loads(self._arguments), indent=2))
json.loads(frame.arguments), indent=2))
print(f"--> {pretty_json}\n")
if not self._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.")
fn = getattr(self, self._function_name)
result = fn(json.loads(self._arguments))
self._function_name = ""
self._arguments = ""
if not frame.function_name in 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, frame.function_name)
result = fn(json.loads(frame.arguments))
if not this_step['run_async']:
if result:
current_step += 1