Skip to content

Commit bdb9ba3

Browse files
authored
Merge pull request #78 from OpenBB-finance/feat/add-agent-dashboard-widgets-example
2 parents ef5f8af + 44b7097 commit bdb9ba3

15 files changed

Lines changed: 962 additions & 14 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Example agent for financial prompt optimization
2+
3+
This is a minimal example agent, powered by OpenAI, that optimizes a user's
4+
financial prompt for clarity, specificity, and actionability. It has no widget
5+
integration and focuses solely on improving prompts.
6+
7+
## Getting started
8+
9+
Here's how to get your agent up and running:
10+
11+
### Prerequisites
12+
13+
Ensure you have poetry, a tool for dependency management and packaging in
14+
Python, as well as your OpenAI API key.
15+
16+
### Installation and Running
17+
18+
1. Clone this repository to your local machine.
19+
20+
2. Install the necessary dependencies:
21+
22+
```sh
23+
poetry install --no-root
24+
```
25+
26+
3. Set the OpenAI API key as an environment variable:
27+
28+
```sh
29+
export OPENAI_API_KEY=<your-api-key>
30+
```
31+
32+
4. Start the API server:
33+
34+
```sh
35+
cd financial-prompt-optimizer
36+
poetry run uvicorn main:app --port 7777 --reload
37+
```
38+
39+
This command runs the FastAPI application, making it accessible on your network.
40+
41+
### Accessing the Documentation
42+
43+
Once the API server is running, you can view the documentation and interact with
44+
the API by visiting: [http://localhost:7777/docs](http://localhost:7777/docs)
45+
46+
### Using with OpenBB Workspace (Optional)
47+
48+
- The agent descriptor is available at: [http://localhost:7777/agents.json](http://localhost:7777/agents.json)
49+
- Features are set to:
50+
- `streaming: true`
51+
- `widget-dashboard-select: false`
52+
- `widget-dashboard-search: false`
53+
54+
### Expected Behavior
55+
56+
When you send a user message, the agent streams a single answer containing two
57+
sections:
58+
59+
- `Optimized Prompt: <single improved prompt>`
60+
- `Rationale: <1–3 short bullets on what changed and why>`
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
from typing import AsyncGenerator
2+
import openai
3+
4+
from fastapi import FastAPI
5+
from fastapi.middleware.cors import CORSMiddleware
6+
from fastapi.responses import JSONResponse
7+
from sse_starlette.sse import EventSourceResponse
8+
9+
from openbb_ai.models import MessageChunkSSE, QueryRequest
10+
from openbb_ai import message_chunk
11+
12+
from openai.types.chat import (
13+
ChatCompletionMessageParam,
14+
ChatCompletionUserMessageParam,
15+
ChatCompletionAssistantMessageParam,
16+
ChatCompletionSystemMessageParam,
17+
)
18+
19+
20+
app = FastAPI()
21+
22+
app.add_middleware(
23+
CORSMiddleware,
24+
allow_origins=["*"],
25+
allow_credentials=True,
26+
allow_methods=["*"],
27+
allow_headers=["*"],
28+
)
29+
30+
31+
@app.get("/agents.json")
32+
def get_copilot_description():
33+
"""Agent descriptor for the OpenBB Workspace."""
34+
return JSONResponse(
35+
content={
36+
"financial_prompt_optimizer": {
37+
"name": "Financial Prompt Optimizer",
38+
"description": "Optimizes a user's prompt for finance: clearer, more specific, and actionable.",
39+
"image": "https://github.com/OpenBB-finance/copilot-for-terminal-pro/assets/14093308/7da2a512-93b9-478d-90bc-b8c3dd0cabcf",
40+
"endpoints": {"query": "http://localhost:7777/v1/query"},
41+
"features": {
42+
"streaming": True,
43+
"widget-dashboard-select": False,
44+
"widget-dashboard-search": False,
45+
},
46+
}
47+
}
48+
)
49+
50+
51+
@app.post("/v1/query")
52+
async def query(request: QueryRequest) -> EventSourceResponse:
53+
"""Stream a concise optimized prompt and rationale."""
54+
55+
openai_messages: list[ChatCompletionMessageParam] = [
56+
ChatCompletionSystemMessageParam(
57+
role="system",
58+
content=(
59+
"You are a concise Financial Prompt Optimizer.\n"
60+
"Rewrite the user's prompt to be clearer, more specific, and immediately actionable for financial analysis.\n"
61+
"Always return exactly the improved prompt:\n"
62+
"Optimized Prompt: <detailed improved prompt with step-by-step>\n"
63+
),
64+
)
65+
]
66+
67+
for message in request.messages:
68+
if message.role == "human":
69+
openai_messages.append(
70+
ChatCompletionUserMessageParam(role="user", content=message.content)
71+
)
72+
elif message.role == "ai" and isinstance(message.content, str):
73+
openai_messages.append(
74+
ChatCompletionAssistantMessageParam(
75+
role="assistant", content=message.content
76+
)
77+
)
78+
79+
async def execution_loop() -> AsyncGenerator[MessageChunkSSE, None]:
80+
client = openai.AsyncOpenAI()
81+
async for event in await client.chat.completions.create(
82+
model="gpt-4o",
83+
messages=openai_messages,
84+
stream=True,
85+
):
86+
if chunk := event.choices[0].delta.content:
87+
yield message_chunk(chunk)
88+
89+
return EventSourceResponse(
90+
content=(
91+
event.model_dump(exclude_none=True) async for event in execution_loop()
92+
),
93+
media_type="text/event-stream",
94+
)
95+
96+
97+
if __name__ == "__main__":
98+
import uvicorn
99+
100+
uvicorn.run("main:app", host="0.0.0.0", port=7777, reload=True)

30-vanilla-agent-raw-widget-data/vanilla_agent_raw_context/main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ async def query(request: QueryRequest) -> EventSourceResponse:
6060
and request.widgets.primary
6161
):
6262
widget_requests: list[WidgetRequest] = []
63+
# Note: If we wanted to iterate through the widgets on the dashboard
64+
# rather than on the widgets on the explicit context
65+
# then we would need to iterate through request.widgets.secondary
66+
# and the agents.json would need "widget-dashboard-search": True
6367
for widget in request.widgets.primary:
6468
widget_requests.append(
6569
WidgetRequest(

31-vanilla-agent-reasoning-steps/vanilla_agent_reasoning_steps/main.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
app.add_middleware(
2323
CORSMiddleware,
24-
allow_origins=["https://pro.openbb.co"],
24+
allow_origins=["*"],
2525
allow_credentials=True,
2626
allow_methods=["*"],
2727
allow_headers=["*"],
@@ -101,13 +101,6 @@ async def execution_loop() -> AsyncGenerator[MessageChunkSSE, None]:
101101
if chunk := event.choices[0].delta.content:
102102
yield message_chunk(chunk).model_dump()
103103

104-
# Reasoning steps can be yielded from anywhere, as long as they are
105-
# yielded from the execution loop back to the OpenBB Workspace.
106-
yield reasoning_step(
107-
event_type="INFO",
108-
message="Answering complete!",
109-
).model_dump()
110-
111104
return EventSourceResponse(
112105
content=execution_loop(),
113106
media_type="text/event-stream",
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Example agent that can handle PDF data with citations
2+
3+
This is an example agent, powered by OpenAI, that can perform question answering
4+
using data retrieved from widgets on OpenBB Workspace. It specifically demonstrates
5+
how to handle PDF data with citation support, including highlighting specific text passages.
6+
7+
## Getting started
8+
9+
Here's how to get your agent up and running:
10+
11+
### Prerequisites
12+
13+
Ensure you have poetry, a tool for dependency management and packaging in
14+
Python, as well as your OpenAI API key.
15+
16+
### Installation and Running
17+
18+
1. Clone this repository to your local machine.
19+
20+
2. Set the OpenAI API key as an environment variable in your .bashrc or .zshrc file:
21+
22+
``` sh
23+
# in .zshrc or .bashrc
24+
export OPENAI_API_KEY=<your-api-key>
25+
```
26+
27+
3. Install the necessary dependencies:
28+
29+
``` sh
30+
poetry install --no-root
31+
```
32+
33+
4.Start the API server:
34+
35+
``` sh
36+
cd 36-vanilla-pdf-citations
37+
poetry run uvicorn vanilla_agent_pdf_citations.main:app --port 7777 --reload
38+
```
39+
40+
This command runs the FastAPI application, making it accessible on your network.
41+
42+
### Testing the Agent
43+
44+
The example agent has a small, basic test suite to ensure it's
45+
working correctly. As you develop your agent, you are highly encouraged to
46+
expand these tests.
47+
48+
You can run the tests with:
49+
50+
```sh
51+
pytest tests
52+
```
53+
54+
### Accessing the Documentation
55+
56+
Once the API server is running, you can view the documentation and interact with
57+
the API by visiting: http://localhost:7777/docs

36-vanilla-agent-pdf-citations/tests/__init__.py

Whitespace-only changes.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import json
2+
from unittest import mock
3+
from fastapi.testclient import TestClient
4+
from vanilla_agent_pdf.main import app
5+
import pytest
6+
from pathlib import Path
7+
from openbb_ai.testing import CopilotResponse
8+
9+
test_client = TestClient(app)
10+
11+
12+
@pytest.fixture(autouse=True)
13+
def reset_sse_starlette_appstatus_event():
14+
"""
15+
Fixture that resets the appstatus event in the sse_starlette app.
16+
Should be used on any test that uses sse_starlette to stream events.
17+
"""
18+
# See https://github.com/sysid/sse-starlette/issues/59
19+
from sse_starlette.sse import AppStatus
20+
21+
AppStatus.should_exit_event = None
22+
23+
24+
def test_query():
25+
test_payload_path = (
26+
Path(__file__).parent.parent.parent
27+
/ "testing"
28+
/ "test_payloads"
29+
/ "single_message.json"
30+
)
31+
test_payload = json.load(open(test_payload_path))
32+
33+
response = test_client.post("/v1/query", json=test_payload)
34+
assert response.status_code == 200
35+
copilot_response = CopilotResponse(response.text)
36+
(copilot_response.has_any("copilotMessage", "2"))
37+
38+
39+
def test_query_conversation():
40+
test_payload_path = (
41+
Path(__file__).parent.parent.parent
42+
/ "testing"
43+
/ "test_payloads"
44+
/ "multiple_messages.json"
45+
)
46+
test_payload = json.load(open(test_payload_path))
47+
48+
response = test_client.post("/v1/query", json=test_payload)
49+
assert response.status_code == 200
50+
copilot_response = CopilotResponse(response.text)
51+
(copilot_response.has_any("copilotMessage", "4"))
52+
53+
54+
def test_query_no_messages():
55+
test_payload = {
56+
"messages": [],
57+
}
58+
response = test_client.post("/v1/query", json=test_payload)
59+
"messages list cannot be empty" in response.text
60+
61+
62+
def test_query_completes_remote_function_call_with_pdf_url():
63+
test_payload_path = (
64+
Path(__file__).parent.parent.parent
65+
/ "testing"
66+
/ "test_payloads"
67+
/ "message_with_primary_widget_and_tool_call_pdf_url.json"
68+
)
69+
test_payload = json.load(open(test_payload_path))
70+
71+
with open(
72+
Path(__file__).parent.parent.parent
73+
/ "testing"
74+
/ "test_payloads"
75+
/ "openbb_story.pdf",
76+
"rb",
77+
) as pdf:
78+
pdf_content = pdf.read()
79+
80+
with mock.patch("vanilla_agent_pdf.main._download_file", return_value=pdf_content):
81+
response = test_client.post("/v1/query", json=test_payload)
82+
83+
assert response.status_code == 200
84+
copilot_response = CopilotResponse(response.text)
85+
(copilot_response.starts("copilotMessage").with_("Didier Lopes").with_("Gamestonk"))

36-vanilla-agent-pdf-citations/vanilla_agent_pdf_citations/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)