-
Notifications
You must be signed in to change notification settings - Fork 84
Expand file tree
/
Copy pathmain.py
More file actions
138 lines (120 loc) · 4.94 KB
/
Copy pathmain.py
File metadata and controls
138 lines (120 loc) · 4.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
from typing import AsyncGenerator
import openai
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from sse_starlette.sse import EventSourceResponse
from openbb_ai.models import MessageChunkSSE, QueryRequest
from openbb_ai import get_widget_data, WidgetRequest, message_chunk
from openai.types.chat import (
ChatCompletionMessageParam,
ChatCompletionUserMessageParam,
ChatCompletionAssistantMessageParam,
ChatCompletionSystemMessageParam,
)
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://pro.openbb.co"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/agents.json")
def get_copilot_description():
"""Widgets configuration file for the OpenBB Terminal Pro"""
return JSONResponse(
content={
"vanilla_agent_raw_context": {
"name": "Vanilla Agent Raw Context",
"description": "A vanilla agent that automatically retrieves widget data and passes it as raw context to the LLM.",
"image": "https://github.com/OpenBB-finance/copilot-for-terminal-pro/assets/14093308/7da2a512-93b9-478d-90bc-b8c3dd0cabcf",
"endpoints": {"query": "http://localhost:7777/v1/query"},
"features": {
"streaming": True,
"widget-dashboard-select": True,
"widget-dashboard-search": False,
},
}
}
)
@app.post("/v1/query")
async def query(request: QueryRequest) -> EventSourceResponse:
"""Query the Copilot."""
# We only automatically fetch widget data if the last message is from a
# human, and widgets have been explicitly added to the request.
if (
request.messages[-1].role == "human"
and request.widgets
and request.widgets.primary
):
widget_requests: list[WidgetRequest] = []
# Note: If we wanted to iterate through the widgets on the dashboard
# rather than on the widgets on the explicit context
# then we would need to iterate through request.widgets.secondary
# and the agents.json would need "widget-dashboard-search": True
for widget in request.widgets.primary:
widget_requests.append(
WidgetRequest(
widget=widget,
input_arguments={
param.name: param.current_value for param in widget.params
},
)
)
async def retrieve_widget_data():
yield get_widget_data(widget_requests).model_dump()
# Early exit to retrieve widget data
return EventSourceResponse(
content=retrieve_widget_data(),
media_type="text/event-stream",
)
# Format the messages into a list of OpenAI messages
openai_messages: list[ChatCompletionMessageParam] = [
ChatCompletionSystemMessageParam(
role="system",
content="You are a helpful financial assistant. Your name is 'Vanilla Agent'.",
)
]
context_str = ""
for index, message in enumerate(request.messages):
if message.role == "human":
openai_messages.append(
ChatCompletionUserMessageParam(role="user", content=message.content)
)
elif message.role == "ai":
if isinstance(message.content, str):
openai_messages.append(
ChatCompletionAssistantMessageParam(
role="assistant", content=message.content
)
)
# We only add the most recent tool call / widget data to context. We do
# this **only for this particular example** to prevent
# previously-retrieved widget data from piling up and exceeding the
# context limit of the LLM.
elif message.role == "tool" and index == len(request.messages) - 1:
context_str += "Use the following data to answer the question:\n\n"
result_str = "--- Data ---\n"
for result in message.data:
for item in result.items:
result_str += f"{item.content}\n"
result_str += "------\n"
context_str += result_str
if context_str:
openai_messages[-1]["content"] += "\n\n" + context_str # type: ignore
# Define the execution loop.
async def execution_loop() -> AsyncGenerator[MessageChunkSSE, None]:
client = openai.AsyncOpenAI()
async for event in await client.chat.completions.create(
model="gpt-4o",
messages=openai_messages,
stream=True,
):
if chunk := event.choices[0].delta.content:
yield message_chunk(chunk).model_dump()
# Stream the SSEs back to the client.
return EventSourceResponse(
content=execution_loop(),
media_type="text/event-stream",
)