tools frame support and wip message resetting/loading
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ from dotenv import load_dotenv
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
from runner import configure
|
from runner import configure
|
||||||
|
|
||||||
|
from pipecat.frames.frames import LLMMessagesUpdateFrame
|
||||||
from pipecat.pipeline.pipeline import Pipeline
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
from pipecat.pipeline.runner import PipelineRunner
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
@@ -37,18 +39,34 @@ logger.add(sys.stderr, level="DEBUG")
|
|||||||
|
|
||||||
|
|
||||||
async def fetch_weather_from_api(function_name, tool_call_id, args, llm, context, result_callback):
|
async def fetch_weather_from_api(function_name, tool_call_id, args, llm, context, result_callback):
|
||||||
|
temperature = 75 if args["format"] == "fahrenheit" else 24
|
||||||
await result_callback(
|
await result_callback(
|
||||||
{
|
{
|
||||||
"conditions": "nice",
|
"conditions": "nice",
|
||||||
"temperature": "75",
|
"temperature": temperature,
|
||||||
|
"format": args["format"],
|
||||||
"timestamp": datetime.now().strftime("%Y%m%d_%H%M%S"),
|
"timestamp": datetime.now().strftime("%Y%m%d_%H%M%S"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_saved_conversation_filenames(
|
||||||
|
function_name, tool_call_id, args, llm, context, result_callback
|
||||||
|
):
|
||||||
|
pattern = re.compile("example_19_\\d{8}_\\d{6}\\.json$")
|
||||||
|
matching_files = []
|
||||||
|
|
||||||
|
for filename in os.listdir("."):
|
||||||
|
if pattern.match(filename):
|
||||||
|
matching_files.append(filename)
|
||||||
|
|
||||||
|
await result_callback({"filenames": matching_files})
|
||||||
|
|
||||||
|
|
||||||
async def save_conversation(function_name, tool_call_id, args, llm, context, result_callback):
|
async def save_conversation(function_name, tool_call_id, args, llm, context, result_callback):
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
filename = f"example_19_{timestamp}.json"
|
filename = f"example_19_{timestamp}.json"
|
||||||
|
logger.debug(f"writing conversation to {filename}\n{json.dumps(context.messages, indent=4)}")
|
||||||
try:
|
try:
|
||||||
with open(filename, "w") as file:
|
with open(filename, "w") as file:
|
||||||
json.dump(context.messages, file, indent=4)
|
json.dump(context.messages, file, indent=4)
|
||||||
@@ -57,6 +75,18 @@ async def save_conversation(function_name, tool_call_id, args, llm, context, res
|
|||||||
await result_callback({"success": False, "error": str(e)})
|
await result_callback({"success": False, "error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
async def load_conversation(function_name, tool_call_id, args, llm, context, result_callback):
|
||||||
|
filename = args["filename"]
|
||||||
|
logger.debug(f"loading conversation from {filename}")
|
||||||
|
try:
|
||||||
|
with open(filename, "r") as file:
|
||||||
|
messages = json.load(file)
|
||||||
|
await result_callback({"success": True})
|
||||||
|
await llm.push_frame(LLMMessagesUpdateFrame(messages))
|
||||||
|
except Exception as e:
|
||||||
|
await result_callback({"success": False, "error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
tools = [
|
tools = [
|
||||||
{
|
{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
@@ -88,6 +118,31 @@ tools = [
|
|||||||
"required": [],
|
"required": [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"name": "get_saved_conversation_filenames",
|
||||||
|
"description": "Get a list of saved conversation histories. Returns a list of filenames. Each filename includes a timestamp. Each file is conversation history that can be loaded into this session.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"name": "load_conversation",
|
||||||
|
"description": "Load a conversation history. Use this function to load a conversation history into the current session.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"filename": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The filename of the conversation history to load.",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["filename"],
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -118,7 +173,7 @@ async def main():
|
|||||||
# turn_detection=TurnDetection(silence_duration_ms=1000),
|
# turn_detection=TurnDetection(silence_duration_ms=1000),
|
||||||
# Or set to False to disable openai turn detection and use transport VAD
|
# Or set to False to disable openai turn detection and use transport VAD
|
||||||
turn_detection=False,
|
turn_detection=False,
|
||||||
tools=tools,
|
# tools=tools,
|
||||||
instructions="""
|
instructions="""
|
||||||
Your knowledge cutoff is 2023-10. You are a helpful and friendly AI.
|
Your knowledge cutoff is 2023-10. You are a helpful and friendly AI.
|
||||||
|
|
||||||
@@ -145,10 +200,14 @@ Remember, your responses should be short. Just one or two sentences, usually.
|
|||||||
# llm.register_function(None, fetch_weather_from_api)
|
# llm.register_function(None, fetch_weather_from_api)
|
||||||
llm.register_function("get_current_weather", fetch_weather_from_api)
|
llm.register_function("get_current_weather", fetch_weather_from_api)
|
||||||
llm.register_function("save_conversation", save_conversation)
|
llm.register_function("save_conversation", save_conversation)
|
||||||
|
llm.register_function("get_saved_conversation_filenames", get_saved_conversation_filenames)
|
||||||
|
llm.register_function("load_conversation", load_conversation)
|
||||||
|
|
||||||
context = OpenAILLMContext(
|
context = OpenAILLMContext(
|
||||||
# [{"role": "user", "content": "What's the weather right now in San Francisco?"}], tools
|
|
||||||
[{"role": "user", "content": "Say 'hello'."}],
|
[{"role": "user", "content": "Say 'hello'."}],
|
||||||
|
# [{"role": "user", "content": "What's the weather right now in San Francisco?"}],
|
||||||
|
# conversation load from file is a WIP -- not functional yet
|
||||||
|
# [{"role": "user", "content": "Load the most recent conversation."}],
|
||||||
tools,
|
tools,
|
||||||
)
|
)
|
||||||
context_aggregator = llm.create_context_aggregator(context)
|
context_aggregator = llm.create_context_aggregator(context)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import json
|
|||||||
import traceback
|
import traceback
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import websockets
|
import websockets
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -18,6 +19,7 @@ from pipecat.frames.frames import (
|
|||||||
LLMFullResponseEndFrame,
|
LLMFullResponseEndFrame,
|
||||||
LLMFullResponseStartFrame,
|
LLMFullResponseStartFrame,
|
||||||
LLMMessagesUpdateFrame,
|
LLMMessagesUpdateFrame,
|
||||||
|
LLMSetToolsFrame,
|
||||||
LLMUpdateSettingsFrame,
|
LLMUpdateSettingsFrame,
|
||||||
StartFrame,
|
StartFrame,
|
||||||
StartInterruptionFrame,
|
StartInterruptionFrame,
|
||||||
@@ -78,6 +80,9 @@ class OpenAIRealtimeLLMContext(OpenAILLMContext):
|
|||||||
# "conversation items" that have been created by opeanai realtime api events but are
|
# "conversation items" that have been created by opeanai realtime api events but are
|
||||||
# not completely filled in, yet. map from item_id to message
|
# not completely filled in, yet. map from item_id to message
|
||||||
self._messages_in_progress = {}
|
self._messages_in_progress = {}
|
||||||
|
# count of messages prior to recent reset
|
||||||
|
self._messages_reset_count = 0
|
||||||
|
self._tools_list_updated = True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def upgrade_to_realtime(obj: OpenAILLMContext) -> "OpenAIRealtimeLLMContext":
|
def upgrade_to_realtime(obj: OpenAILLMContext) -> "OpenAIRealtimeLLMContext":
|
||||||
@@ -86,30 +91,48 @@ class OpenAIRealtimeLLMContext(OpenAILLMContext):
|
|||||||
obj.__setup_local()
|
obj.__setup_local()
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
# cases to handle
|
# still working on
|
||||||
# - tools in the context constructor (and in general?)
|
# - clearing the context by deleting all messages
|
||||||
# - relatedly, set tools frame
|
# - reloading from a standard messages list
|
||||||
# - clearing the context by deleting all messages (for scripted conversations)
|
|
||||||
# - truncating the last spoken message to maintain context when interrupted
|
# - truncating the last spoken message to maintain context when interrupted
|
||||||
|
|
||||||
|
def set_tools(self, tools: List):
|
||||||
|
super().set_tools(tools)
|
||||||
|
self._tools_list_updated = True
|
||||||
|
|
||||||
def add_message(self, message):
|
def add_message(self, message):
|
||||||
super().add_message(message)
|
super().add_message(message)
|
||||||
self._unsent_messages.append(message)
|
self._unsent_messages.append(message)
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
def add_messages(self, messages):
|
||||||
|
super().add_messages(messages)
|
||||||
|
self._unsent_messages.extend(messages)
|
||||||
|
|
||||||
def add_message_already_present_in_api_context(self, message):
|
def add_message_already_present_in_api_context(self, message):
|
||||||
super().add_message(message)
|
super().add_message(message)
|
||||||
return message
|
return message
|
||||||
|
|
||||||
def set_messages(self, messages):
|
def set_messages(self, messages):
|
||||||
|
self._messages_reset_count = len(self.messages) - len(self._unsent_messages)
|
||||||
super().set_messages(messages)
|
super().set_messages(messages)
|
||||||
self._unsent_messages = deepcopy(self._messages)
|
self._unsent_messages = deepcopy(self._messages)
|
||||||
|
|
||||||
def get_unsent_messages(self):
|
def get_unsent_messages(self):
|
||||||
return self._unsent_messages
|
return self._unsent_messages
|
||||||
|
|
||||||
|
def get_messages_reset_count(self):
|
||||||
|
return self._messages_reset_count
|
||||||
|
|
||||||
|
def get_tools_list_updated(self):
|
||||||
|
return self._tools_list_updated
|
||||||
|
|
||||||
def update_all_messages_sent(self):
|
def update_all_messages_sent(self):
|
||||||
self._unsent_messages = []
|
self._unsent_messages = []
|
||||||
|
self._messages_reset_count = 0
|
||||||
|
|
||||||
|
def update_tools_list_sent(self):
|
||||||
|
self._tools_list_updated = False
|
||||||
|
|
||||||
def note_manually_added_message(self, item_id):
|
def note_manually_added_message(self, item_id):
|
||||||
self._manually_created_messages[item_id] = True
|
self._manually_created_messages[item_id] = True
|
||||||
@@ -163,6 +186,10 @@ class OpenAIRealtimeUserContextAggregator(OpenAIUserContextAggregator):
|
|||||||
if isinstance(frame, LLMMessagesUpdateFrame):
|
if isinstance(frame, LLMMessagesUpdateFrame):
|
||||||
await self.push_frame(_InternalMessagesUpdateFrame(context=self._context))
|
await self.push_frame(_InternalMessagesUpdateFrame(context=self._context))
|
||||||
|
|
||||||
|
# Parent also doesn't push the LLMSetToolsFrame.
|
||||||
|
if isinstance(frame, LLMSetToolsFrame):
|
||||||
|
await self.push_frame(frame, direction)
|
||||||
|
|
||||||
async def _push_aggregation(self):
|
async def _push_aggregation(self):
|
||||||
# for the moment, ignore all user input coming into the pipeline.
|
# for the moment, ignore all user input coming into the pipeline.
|
||||||
# todo: think about whether/how to fix this to allow for text input from
|
# todo: think about whether/how to fix this to allow for text input from
|
||||||
@@ -232,7 +259,7 @@ class OpenAILLMServiceRealtimeBeta(LLMService):
|
|||||||
self.api_key = api_key
|
self.api_key = api_key
|
||||||
self.base_url = base_url
|
self.base_url = base_url
|
||||||
|
|
||||||
self._session_properties = session_properties
|
self._session_properties: events.SessionProperties = session_properties
|
||||||
self._audio_input_paused = start_audio_paused
|
self._audio_input_paused = start_audio_paused
|
||||||
self._send_transcription_frames = send_transcription_frames
|
self._send_transcription_frames = send_transcription_frames
|
||||||
# todo: wire _send_user_started_speaking_frames up correctly
|
# todo: wire _send_user_started_speaking_frames up correctly
|
||||||
@@ -304,7 +331,12 @@ class OpenAILLMServiceRealtimeBeta(LLMService):
|
|||||||
return self._websocket
|
return self._websocket
|
||||||
raise Exception("Websocket not connected")
|
raise Exception("Websocket not connected")
|
||||||
|
|
||||||
async def _update_settings(self, settings: events.SessionProperties):
|
async def _update_settings(self):
|
||||||
|
settings = self._session_properties
|
||||||
|
# tools given in the context override the tools in the session properties
|
||||||
|
if self._context and self._context.tools:
|
||||||
|
settings.tools = self._context.tools
|
||||||
|
self._context.update_tools_list_sent()
|
||||||
await self.send_client_event(events.SessionUpdateEvent(session=settings))
|
await self.send_client_event(events.SessionUpdateEvent(session=settings))
|
||||||
|
|
||||||
async def _receive_task_handler(self):
|
async def _receive_task_handler(self):
|
||||||
@@ -315,7 +347,7 @@ class OpenAILLMServiceRealtimeBeta(LLMService):
|
|||||||
if evt.type == "session.created":
|
if evt.type == "session.created":
|
||||||
# session.created is received right after connecting. send a message
|
# session.created is received right after connecting. send a message
|
||||||
# to configure the session properties.
|
# to configure the session properties.
|
||||||
await self._update_settings(self._session_properties)
|
await self._update_settings()
|
||||||
elif evt.type == "session.updated":
|
elif evt.type == "session.updated":
|
||||||
self._session_properties = evt.session
|
self._session_properties = evt.session
|
||||||
elif evt.type == "input_audio_buffer.speech_started":
|
elif evt.type == "input_audio_buffer.speech_started":
|
||||||
@@ -445,9 +477,12 @@ class OpenAILLMServiceRealtimeBeta(LLMService):
|
|||||||
f"The LLM tried to call a function named '{function_name}', but there isn't a callback registered for that function."
|
f"The LLM tried to call a function named '{function_name}', but there isn't a callback registered for that function."
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _reset_conversation(self):
|
async def _reset_conversation(self, count):
|
||||||
# need to think about how to implement this, and how to think about interop with messages lists
|
# need to think about how to implement this, and how to think about interop with messages lists
|
||||||
# used with the HTTP API
|
# used with the HTTP API
|
||||||
|
logger.debug(f"!!! RESET CONVERSATION: {count} [WIP]")
|
||||||
|
await self._disconnect()
|
||||||
|
await self._connect()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def _send_messages_context_update(self):
|
async def _send_messages_context_update(self):
|
||||||
@@ -455,28 +490,70 @@ class OpenAILLMServiceRealtimeBeta(LLMService):
|
|||||||
return
|
return
|
||||||
context = self._context
|
context = self._context
|
||||||
messages = context.get_unsent_messages()
|
messages = context.get_unsent_messages()
|
||||||
|
|
||||||
|
needs_reset = context.get_messages_reset_count()
|
||||||
context.update_all_messages_sent()
|
context.update_all_messages_sent()
|
||||||
|
|
||||||
|
if needs_reset:
|
||||||
|
await self._reset_conversation(needs_reset)
|
||||||
|
# debugging
|
||||||
|
logger.debug("MESSAGE HISTORY RELOAD NOT IMPLEMENTED YET")
|
||||||
|
return
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
for m in messages:
|
for m in messages:
|
||||||
if m and (m.get("role") == "user" or m.get("role") == "system"):
|
if m and (
|
||||||
|
m.get("role") == "user" or m.get("role") == "system" or m.get("role") == "assistant"
|
||||||
|
):
|
||||||
content = m.get("content")
|
content = m.get("content")
|
||||||
if isinstance(content, str):
|
if isinstance(content, str):
|
||||||
items.append(
|
# skip any messages that aren't "text" and change "user" message type to "input_text"
|
||||||
events.ConversationItem(
|
|
||||||
type="message",
|
if m.get("type", "text") == "text":
|
||||||
status="completed",
|
items.append(
|
||||||
role="user",
|
events.ConversationItem(
|
||||||
content=[events.ItemContent(type="input_text", text=content)],
|
type="message",
|
||||||
|
status="completed",
|
||||||
|
role=m.get("role", "user"),
|
||||||
|
content=[
|
||||||
|
events.ItemContent(
|
||||||
|
type="input_text" if m.get("role") == "user" else "text",
|
||||||
|
text=content,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
elif isinstance(content, list):
|
elif isinstance(content, list):
|
||||||
|
# skip any messages that aren't "text" and change "user" message type to "input_text"
|
||||||
|
cs = []
|
||||||
|
for item in content:
|
||||||
|
if item.get("type", "text") == "text":
|
||||||
|
# cs.append(events.ItemContent(type="input_text", text=item.get("text")))
|
||||||
|
(
|
||||||
|
cs.append(
|
||||||
|
events.ItemContent(
|
||||||
|
type="input_text" if m.get("role") == "user" else "text",
|
||||||
|
text=item.get("text"),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if cs:
|
||||||
|
items.append(
|
||||||
|
events.ConversationItem(
|
||||||
|
type="message",
|
||||||
|
status="completed",
|
||||||
|
role=m.get("role", "user"),
|
||||||
|
content=cs,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif m.get("role") == "assistant" and m.get("tool_calls"):
|
||||||
|
tc = m.get("tool_calls")[0]
|
||||||
items.append(
|
items.append(
|
||||||
events.ConversationItem(
|
events.ConversationItem(
|
||||||
type="message",
|
type="function_call",
|
||||||
status="completed",
|
call_id=tc["id"],
|
||||||
role="user",
|
name=tc["function"]["name"],
|
||||||
content=content,
|
arguments=tc["function"]["arguments"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -489,11 +566,14 @@ class OpenAILLMServiceRealtimeBeta(LLMService):
|
|||||||
output=m["content"],
|
output=m["content"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
context.note_manually_added_message(item.id)
|
context.note_manually_added_message(item.id)
|
||||||
await self.send_client_event(events.ConversationItemCreateEvent(item=item))
|
await self.send_client_event(events.ConversationItemCreateEvent(item=item))
|
||||||
|
|
||||||
async def _create_response(self):
|
async def _create_response(self):
|
||||||
|
if self._context.get_tools_list_updated():
|
||||||
|
await self._update_settings()
|
||||||
await self._send_messages_context_update()
|
await self._send_messages_context_update()
|
||||||
logger.debug(f"Creating response: {self._context.get_messages_for_logging()}")
|
logger.debug(f"Creating response: {self._context.get_messages_for_logging()}")
|
||||||
await self.push_frame(LLMFullResponseStartFrame())
|
await self.push_frame(LLMFullResponseStartFrame())
|
||||||
@@ -544,7 +624,10 @@ class OpenAILLMServiceRealtimeBeta(LLMService):
|
|||||||
self._context = frame.context
|
self._context = frame.context
|
||||||
await self._send_messages_context_update()
|
await self._send_messages_context_update()
|
||||||
elif isinstance(frame, LLMUpdateSettingsFrame):
|
elif isinstance(frame, LLMUpdateSettingsFrame):
|
||||||
await self._update_settings(frame.settings)
|
self._session_properties = frame.settings
|
||||||
|
await self._update_settings()
|
||||||
|
elif isinstance(frame, LLMSetToolsFrame):
|
||||||
|
await self._update_settings()
|
||||||
|
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user