mirror of
https://github.com/hanzo-io/chat.git
synced 2026-10-30 18:57:58 +00:00
396 lines
12 KiB
Python
396 lines
12 KiB
Python
import json
|
|
|
|
import frappe
|
|
from openai import AssistantEventHandler
|
|
from openai.types.beta.threads import Text
|
|
from openai.types.beta.threads.runs import RunStep
|
|
from typing_extensions import override
|
|
|
|
from raven.ai.functions import (
|
|
attach_file_to_document,
|
|
cancel_document,
|
|
create_document,
|
|
create_documents,
|
|
delete_document,
|
|
delete_documents,
|
|
get_amended_document,
|
|
get_document,
|
|
get_documents,
|
|
get_list,
|
|
submit_document,
|
|
update_document,
|
|
update_documents,
|
|
)
|
|
from raven.ai.openai_client import get_open_ai_client
|
|
|
|
|
|
def stream_response(ai_thread_id: str, bot, channel_id: str):
|
|
|
|
client = get_open_ai_client()
|
|
|
|
assistant_id = bot.openai_assistant_id
|
|
|
|
docs_updated = []
|
|
|
|
class EventHandler(AssistantEventHandler):
|
|
@override
|
|
def on_run_step_done(self, run_step: RunStep) -> None:
|
|
details = run_step.step_details
|
|
if details.type == "tool_calls":
|
|
for tool in details.tool_calls:
|
|
if tool.type == "code_interpreter":
|
|
self.publish_event("Running code...")
|
|
if tool.type == "file_search":
|
|
self.publish_event("Searching file contents...")
|
|
else:
|
|
self.publish_event("Raven AI is thinking...")
|
|
|
|
@override
|
|
def on_text_done(self, text: Text):
|
|
link_doctype = None
|
|
link_document = None
|
|
if len(docs_updated) == 1:
|
|
link_doctype = docs_updated[0]["doctype"]
|
|
link_document = docs_updated[0]["document_id"]
|
|
|
|
file_urls = []
|
|
|
|
# Check if there are any annotations with files
|
|
annotations = text.annotations
|
|
for annotation in annotations:
|
|
if annotation.type == "file_path":
|
|
file_path = annotation.file_path
|
|
file_id = file_path.file_id
|
|
file_name = annotation.text.split("/")[-1]
|
|
file_content = client.files.content(file_id)
|
|
|
|
file_doc = frappe.get_doc(
|
|
{
|
|
"doctype": "File",
|
|
"file_name": file_name,
|
|
"content": file_content.read(),
|
|
"is_private": True,
|
|
}
|
|
)
|
|
file_doc.insert()
|
|
file_urls.append(
|
|
{
|
|
"url": file_doc.file_url,
|
|
"text": annotation.text,
|
|
}
|
|
)
|
|
|
|
if file_urls:
|
|
# If there are amy file URLs, replace the content with the URL and send the file as a separate message
|
|
for file_url in file_urls:
|
|
text.value = text.value.replace(file_url["text"], file_url["url"])
|
|
|
|
bot.send_message(
|
|
channel_id=channel_id,
|
|
text=text.value,
|
|
link_doctype=link_doctype,
|
|
link_document=link_document,
|
|
markdown=True,
|
|
)
|
|
|
|
for file_url in file_urls:
|
|
bot.send_message(channel_id=channel_id, file=file_url["url"])
|
|
|
|
else:
|
|
bot.send_message(
|
|
channel_id=channel_id,
|
|
text=text.value,
|
|
link_doctype=link_doctype,
|
|
link_document=link_document,
|
|
markdown=True,
|
|
)
|
|
|
|
frappe.publish_realtime(
|
|
"ai_event_clear",
|
|
{
|
|
"channel_id": channel_id,
|
|
},
|
|
doctype="Raven Channel",
|
|
docname=channel_id,
|
|
after_commit=True,
|
|
)
|
|
|
|
@override
|
|
def on_event(self, event):
|
|
# Handle image and file outputs
|
|
if event.event == "thread.message.delta":
|
|
if hasattr(event.data, "delta") and hasattr(event.data.delta, "content"):
|
|
for content in event.data.delta.content:
|
|
if content.type == "image_file":
|
|
# Handle image file
|
|
file_id = content.image_file.file_id
|
|
file = client.files.retrieve(file_id)
|
|
file_content = client.files.content(file_id)
|
|
|
|
# Save the file content
|
|
file_doc = frappe.new_doc("File")
|
|
file_doc.file_name = file.filename + ".png"
|
|
file_doc.content = file_content.read()
|
|
file_doc.is_private = True
|
|
file_doc.insert()
|
|
|
|
bot.send_message(channel_id=channel_id, file=file_doc.file_url)
|
|
|
|
# Handle tool calls
|
|
elif event.event == "thread.run.requires_action":
|
|
run_id = event.data.id
|
|
self.handle_requires_action(event.data, run_id)
|
|
|
|
def publish_event(self, text):
|
|
frappe.publish_realtime(
|
|
"ai_event",
|
|
{
|
|
"text": text,
|
|
"channel_id": channel_id,
|
|
"bot": bot.name,
|
|
},
|
|
doctype="Raven Channel",
|
|
docname=channel_id,
|
|
)
|
|
|
|
def handle_requires_action(self, data, run_id):
|
|
tool_outputs = []
|
|
|
|
for tool in data.required_action.submit_tool_outputs.tool_calls:
|
|
|
|
function = None
|
|
|
|
try:
|
|
function = frappe.get_cached_doc("Raven AI Function", tool.function.name)
|
|
except frappe.DoesNotExistError:
|
|
tool_outputs.append({"tool_call_id": tool.id, "output": "Function not found"})
|
|
return
|
|
|
|
# When calling the function, we need to pass the arguments as named params/json
|
|
# Args is a dictionary of the form {"param_name": "param_value"}
|
|
|
|
try:
|
|
args = json.loads(tool.function.arguments)
|
|
|
|
# Check the type of function and then call it accordingly
|
|
|
|
function_output = {}
|
|
|
|
if function.type == "Custom Function":
|
|
function_name = frappe.get_attr(function.function_path)
|
|
|
|
if bot.allow_bot_to_write_documents:
|
|
# We can commit to the database if writes are allowed
|
|
if function.pass_parameters_as_json:
|
|
function_output = function_name(args)
|
|
else:
|
|
function_output = function_name(**args)
|
|
else:
|
|
# We need to savepoint and then rollback
|
|
frappe.db.savepoint(run_id + "_" + tool.id)
|
|
if function.pass_parameters_as_json:
|
|
function_output = function_name(args)
|
|
else:
|
|
function_output = function_name(**args)
|
|
frappe.db.rollback(save_point=run_id + "_" + tool.id)
|
|
|
|
if function.type == "Get Document":
|
|
self.publish_event(
|
|
"Fetching {} {}...".format(function.reference_doctype, args.get("document_id"))
|
|
)
|
|
function_output = get_document(function.reference_doctype, **args)
|
|
|
|
if function.type == "Get Multiple Documents":
|
|
self.publish_event(f"Fetching multiple {function.reference_doctype}s...")
|
|
function_output = get_documents(function.reference_doctype, **args)
|
|
|
|
if function.type == "Submit Document":
|
|
self.publish_event(f"Submitting {function.reference_doctype} {args.get('document_id')}...")
|
|
function_output = submit_document(function.reference_doctype, **args)
|
|
|
|
if function.type == "Cancel Document":
|
|
self.publish_event(f"Cancelling {function.reference_doctype} {args.get('document_id')}...")
|
|
function_output = cancel_document(function.reference_doctype, **args)
|
|
|
|
if function.type == "Get Amended Document":
|
|
self.publish_event(
|
|
f"Fetching amended document for {function.reference_doctype} {args.get('document_id')}..."
|
|
)
|
|
function_output = get_amended_document(function.reference_doctype, **args)
|
|
|
|
if function.type == "Delete Document":
|
|
self.publish_event(
|
|
"Deleting {} {}...".format(function.reference_doctype, args.get("document_id"))
|
|
)
|
|
function_output = delete_document(function.reference_doctype, **args)
|
|
|
|
if function.type == "Delete Multiple Documents":
|
|
self.publish_event(f"Deleting multiple {function.reference_doctype}s...")
|
|
function_output = delete_documents(function.reference_doctype, **args)
|
|
|
|
if function.type == "Create Document":
|
|
self.publish_event(f"Creating {function.reference_doctype}...")
|
|
function_output = create_document(function.reference_doctype, data=args, function=function)
|
|
|
|
docs_updated.append(
|
|
{"doctype": function.reference_doctype, "document_id": function_output.get("document_id")}
|
|
)
|
|
|
|
if function.type == "Create Multiple Documents":
|
|
self.publish_event(f"Creating multiple {function.reference_doctype}s...")
|
|
function_output = create_documents(
|
|
function.reference_doctype, data=args.get("data"), function=function
|
|
)
|
|
|
|
for doc_id in function_output.get("documents"):
|
|
docs_updated.append({"doctype": function.reference_doctype, "document_id": doc_id})
|
|
|
|
if function.type == "Update Document":
|
|
self.publish_event(f"Updating {function.reference_doctype}...")
|
|
function_output = update_document(
|
|
function.reference_doctype,
|
|
document_id=args.get("document_id"),
|
|
data=args,
|
|
function=function,
|
|
)
|
|
|
|
docs_updated.append(
|
|
{"doctype": function.reference_doctype, "document_id": args.get("document_id")}
|
|
)
|
|
|
|
if function.type == "Update Multiple Documents":
|
|
self.publish_event(f"Updating multiple {function.reference_doctype}s...")
|
|
function_output = update_documents(
|
|
function.reference_doctype, data=args.get("data"), function=function
|
|
)
|
|
|
|
for doc_id in function_output.get("documents"):
|
|
docs_updated.append({"doctype": function.reference_doctype, "document_id": doc_id})
|
|
|
|
if function.type == "Attach File to Document":
|
|
doctype = args.get("doctype")
|
|
document_id = args.get("document_id")
|
|
file_path = args.get("file_path")
|
|
self.publish_event(f"Attaching file to {doctype} {document_id}...")
|
|
function_output = attach_file_to_document(doctype, document_id, file_path)
|
|
|
|
if function.type == "Get List":
|
|
self.publish_event(f"Fetching list of {function.reference_doctype}...")
|
|
function_output = get_list(
|
|
function.reference_doctype,
|
|
filters=args.get("filters"),
|
|
fields=args.get("fields"),
|
|
limit=args.get("limit", 20),
|
|
)
|
|
|
|
tool_outputs.append(
|
|
{"tool_call_id": tool.id, "output": json.dumps(function_output, default=str)}
|
|
)
|
|
|
|
except Exception as e:
|
|
frappe.log_error("Raven AI Error", frappe.get_traceback())
|
|
|
|
if bot.debug_mode:
|
|
bot.send_message(
|
|
channel_id=channel_id,
|
|
text=f"<details data-summary='Error in function call'><p>{frappe.get_traceback()}</p></details>",
|
|
)
|
|
tool_outputs.append(
|
|
{
|
|
"tool_call_id": tool.id,
|
|
"output": json.dumps(
|
|
{
|
|
"message": "There was an error in the function call",
|
|
"error": frappe.get_traceback(),
|
|
},
|
|
default=str,
|
|
),
|
|
}
|
|
)
|
|
|
|
# Submit all tool_outputs at the same time
|
|
self.submit_tool_outputs(tool_outputs, run_id)
|
|
|
|
def submit_tool_outputs(self, tool_outputs, run_id):
|
|
# Use the submit_tool_outputs_stream helper
|
|
with client.beta.threads.runs.submit_tool_outputs_stream(
|
|
thread_id=self.current_run.thread_id,
|
|
run_id=self.current_run.id,
|
|
tool_outputs=tool_outputs,
|
|
event_handler=EventHandler(),
|
|
) as stream:
|
|
self.publish_event("Raven AI is thinking...")
|
|
for text in stream.text_deltas:
|
|
print(text, end="", flush=True)
|
|
print()
|
|
|
|
# We need to get the instructions from the bot
|
|
instructions = get_instructions(bot)
|
|
with client.beta.threads.runs.stream(
|
|
thread_id=ai_thread_id,
|
|
assistant_id=assistant_id,
|
|
event_handler=EventHandler(),
|
|
instructions=instructions,
|
|
) as stream:
|
|
try:
|
|
stream.until_done()
|
|
except Exception as e:
|
|
frappe.log_error("Raven AI Error", frappe.get_traceback())
|
|
bot.send_message(
|
|
channel_id=channel_id,
|
|
text=f"There was an error in the AI thread. Please try again.<br/>Error: {str(e)}",
|
|
)
|
|
frappe.publish_realtime(
|
|
"ai_event_clear",
|
|
{
|
|
"channel_id": channel_id,
|
|
},
|
|
doctype="Raven Channel",
|
|
docname=channel_id,
|
|
after_commit=True,
|
|
)
|
|
|
|
|
|
def get_instructions(bot):
|
|
|
|
# If no instruction is set, or dynamic instruction is disabled, we return None
|
|
if not bot.instruction or not bot.dynamic_instructions:
|
|
return None
|
|
|
|
vars = get_variables_for_instructions()
|
|
|
|
instructions = frappe.render_template(bot.instruction, vars)
|
|
return instructions
|
|
|
|
|
|
def get_variables_for_instructions():
|
|
user = frappe.get_cached_doc("User", frappe.session.user)
|
|
|
|
employee_company = None
|
|
company = None
|
|
employee_id = None
|
|
department = None
|
|
|
|
if "erpnext" in frappe.get_installed_apps():
|
|
employee_id = frappe.db.exists("Employee", {"user_id": frappe.session.user})
|
|
|
|
if employee_id:
|
|
employee = frappe.get_cached_doc("Employee", employee_id)
|
|
employee_company = employee.company
|
|
department = employee.department
|
|
|
|
company = frappe.defaults.get_user_default("company") or frappe.db.get_single_value(
|
|
"Global Defaults", "default_company"
|
|
)
|
|
|
|
return {
|
|
"first_name": user.first_name,
|
|
"full_name": user.full_name,
|
|
"email": user.email,
|
|
"user_id": frappe.session.user,
|
|
"company": company,
|
|
"employee_id": employee_id,
|
|
"department": department,
|
|
"employee_company": employee_company,
|
|
}
|