Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions 42-vanilla-agent-feedback/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 42 - Vanilla Agent Feedback

A vanilla agent that demonstrates how to receive and persist user feedback (thumbs up/down) from OpenBB Workspace.

## What it does

This agent exposes a `/v1/feedback` endpoint that the Workspace frontend calls when a user gives thumbs up/down on an AI response. Feedback is persisted to a local `feedback.json` file.

The agent declares `"feedback": true` in its `agents.json` features, which signals to the Workspace that this agent supports receiving feedback directly (instead of it going to analytics).

## Feedback payload

```json
{
"vote": "thumbs_up",
"tags": ["Not factually correct / Hallucinations / Inaccurate"],
"user_comment": "Optional additional comment",
"ai_response": "The AI response that was rated",
"user_prompt": "The user's original prompt",
"trace_id": "request-trace-id"
}
```

## Running

```bash
cd 42-vanilla-agent-feedback
poetry run uvicorn vanilla_agent_feedback.main:app --port 7777 --reload
```

Then add `http://localhost:7777` as a custom agent in OpenBB Workspace.

## Endpoints

| Method | Path | Description |
|--------|------|-------------|
| GET | `/agents.json` | Agent manifest |
| POST | `/v1/query` | Chat query (basic LLM passthrough) |
| POST | `/v1/feedback` | Receive user feedback |

## Testing

```bash
poetry run pytest 42-vanilla-agent-feedback/tests
```
23 changes: 23 additions & 0 deletions 42-vanilla-agent-feedback/feedback.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[
{
"timestamp": "2026-04-13T02:22:16.124393+00:00",
"vote": "thumbs_up",
"tags": [],
"user_comment": "",
"ai_response": "Hello! How can I assist you today?",
"user_prompt": "Hi",
"trace_id": ""
},
{
"timestamp": "2026-04-13T02:22:40.944276+00:00",
"vote": "thumbs_down",
"tags": [
"Not factually correct / Hallucinations / Inaccurate",
"Other"
],
"user_comment": "No!\n",
"ai_response": "I'm glad to hear that! What can I help you with today? If you have any questions or need assistance with financial matters, feel free to ask.",
"user_prompt": "cool",
"trace_id": ""
}
]
Empty file.
88 changes: 88 additions & 0 deletions 42-vanilla-agent-feedback/tests/test_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import json
from pathlib import Path

import pytest
from fastapi.testclient import TestClient

from vanilla_agent_feedback.main import FEEDBACK_FILE, app

test_client = TestClient(app)


@pytest.fixture(autouse=True)
def reset_sse_starlette_appstatus_event():
from sse_starlette.sse import AppStatus

AppStatus.should_exit_event = None


@pytest.fixture(autouse=True)
def cleanup_feedback_file():
yield
if FEEDBACK_FILE.exists():
FEEDBACK_FILE.unlink()


def test_agents_json():
response = test_client.get("/agents.json")
assert response.status_code == 200
data = response.json()
assert "vanilla_agent_feedback" in data
agent = data["vanilla_agent_feedback"]
assert agent["features"]["feedback"] is True
assert agent["endpoints"]["feedback"] == "/v1/feedback"


def test_feedback_thumbs_up(tmp_path, monkeypatch):
feedback_file = tmp_path / "feedback.json"
monkeypatch.setattr("vanilla_agent_feedback.main.FEEDBACK_FILE", feedback_file)

payload = {
"vote": "thumbs_up",
"tags": [],
"user_comment": "",
"ai_response": "Test response",
"user_prompt": "Test prompt",
"trace_id": "test-trace-123",
}
response = test_client.post("/v1/feedback", json=payload)
assert response.status_code == 200
assert response.json() == {"status": "ok"}

entries = json.loads(feedback_file.read_text())
assert len(entries) == 1
assert entries[0]["vote"] == "thumbs_up"


def test_feedback_thumbs_down(tmp_path, monkeypatch):
feedback_file = tmp_path / "feedback.json"
monkeypatch.setattr("vanilla_agent_feedback.main.FEEDBACK_FILE", feedback_file)

payload = {
"vote": "thumbs_down",
"tags": ["Not factually correct / Hallucinations / Inaccurate"],
"user_comment": "The data was wrong",
"ai_response": "Test response",
"user_prompt": "Test prompt",
"trace_id": "test-trace-456",
}
response = test_client.post("/v1/feedback", json=payload)
assert response.status_code == 200
assert response.json() == {"status": "ok"}

entries = json.loads(feedback_file.read_text())
assert len(entries) == 1
assert entries[0]["vote"] == "thumbs_down"
assert entries[0]["tags"] == ["Not factually correct / Hallucinations / Inaccurate"]


def test_query():
test_payload_path = (
Path(__file__).parent.parent.parent
/ "testing"
/ "test_payloads"
/ "single_message.json"
)
test_payload = json.load(open(test_payload_path))
response = test_client.post("/v1/query", json=test_payload)
assert response.status_code == 200
Empty file.
120 changes: 120 additions & 0 deletions 42-vanilla-agent-feedback/vanilla_agent_feedback/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import AsyncGenerator

import openai
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from openai.types.chat import (
ChatCompletionAssistantMessageParam,
ChatCompletionMessageParam,
ChatCompletionSystemMessageParam,
ChatCompletionUserMessageParam,
)
from openbb_ai import message_chunk
from openbb_ai.models import MessageChunkSSE, QueryRequest
from pydantic import BaseModel
from sse_starlette.sse import EventSourceResponse

app = FastAPI()

app.add_middleware(
CORSMiddleware,
allow_origins=["https://pro.openbb.co", "http://localhost:1420"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

FEEDBACK_FILE = Path(__file__).parent.parent / "feedback.json"


class FeedbackRequest(BaseModel):
vote: str
tags: list[str] = []
user_comment: str = ""
ai_response: str = ""
user_prompt: str = ""
trace_id: str = ""


@app.get("/agents.json")
def get_copilot_description():
"""Agent configuration for OpenBB Workspace."""
return JSONResponse(
content={
"vanilla_agent_feedback": {
"name": "Vanilla Agent Feedback",
"description": "A vanilla agent that receives and persists user feedback (thumbs up/down).",
"image": "https://github.com/OpenBB-finance/copilot-for-terminal-pro/assets/14093308/7da2a512-93b9-478d-90bc-b8c3dd0cabcf",
"endpoints": {
"query": "/v1/query",
"feedback": "/v1/feedback",
},
"features": {
"streaming": True,
"widget-dashboard-select": False,
"widget-dashboard-search": False,
"feedback": True,
},
}
}
)


@app.post("/v1/feedback")
async def feedback(request: FeedbackRequest):
"""Receive and persist user feedback to a local JSON file."""
entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
**request.model_dump(),
}

entries = []
if FEEDBACK_FILE.exists():
entries = json.loads(FEEDBACK_FILE.read_text())

entries.append(entry)
FEEDBACK_FILE.write_text(json.dumps(entries, indent=2))

return JSONResponse(content={"status": "ok"})


@app.post("/v1/query")
async def query(request: QueryRequest) -> EventSourceResponse:
"""Query the Copilot."""
openai_messages: list[ChatCompletionMessageParam] = [
ChatCompletionSystemMessageParam(
role="system",
content="You are a helpful financial assistant. Your name is 'Vanilla Agent Feedback'.",
)
]

for message in request.messages:
if message.role == "human":
openai_messages.append(
ChatCompletionUserMessageParam(role="user", content=message.content)
)
elif message.role == "ai" and isinstance(message.content, str):
openai_messages.append(
ChatCompletionAssistantMessageParam(
role="assistant", content=message.content
)
)

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()

return EventSourceResponse(
content=execution_loop(),
media_type="text/event-stream",
)
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ It depends heavily on the [OpenBB AI SDK](https://github.com/OpenBB-finance/open
For documentation on how to use the OpenBB AI SDK (highly recommended!), see the [OpenBB AI SDK README](https://github.com/OpenBB-finance/openbb-ai).

## Examples

If you prefer diving straight into code, we have a growing list of examples of custom agents in this repository, varying in complexity and features:

- [A vanilla agent that can improve a user prompt](./financial-prompt-optimizer)
Expand Down
Loading
Loading