diff --git a/content/workspace/developers/ai-features/custom-agent-features.md b/content/workspace/developers/ai-features/custom-agent-features.md
index 662dbbdc6c..7395969ec8 100644
--- a/content/workspace/developers/ai-features/custom-agent-features.md
+++ b/content/workspace/developers/ai-features/custom-agent-features.md
@@ -3,11 +3,11 @@ title: Custom agent features
sidebar_position: 8
description: Configure and manage custom agent features based on workspace options
keywords:
-- features
-- configuration
-- workspace options
-- custom agents
-- SSE
+ - features
+ - configuration
+ - workspace options
+ - custom agents
+ - SSE
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
@@ -18,9 +18,9 @@ Create agents that can dynamically enable or disable features based on workspace
Reference implementation in [this GitHub repository](https://github.com/OpenBB-finance/agents-for-openbb/tree/main/37-vanilla-agent-custom-features).
-
+
-
+
## Architecture
@@ -44,10 +44,29 @@ return JSONResponse(content={
"description": "Allows the copilot to do deep research",
},
"web-search": {
- "label": "Web Search",
+ "label": "Web Search",
"default": True,
"description": "Allows the copilot to search the web.",
},
+ "model": {
+ "label": "Model",
+ "type": "select",
+ "default": "claude-sonnet-4-20250514",
+ "description": "Select the LLM model to use.",
+ "options": [
+ {"label": "Claude Opus 4", "value": "claude-opus-4-0-20250514"},
+ {"label": "Claude Sonnet 4", "value": "claude-sonnet-4-20250514"},
+ {"label": "GPT-4o", "value": "gpt-4o"},
+ {"label": "GPT-4o mini", "value": "gpt-4o-mini"},
+ ],
+ },
+ "agent-name": {
+ "label": "Name of Agent",
+ "type": "text",
+ "default": "Example Agent",
+ "description": "Set the name the agent uses to introduce itself.",
+ "placeholder": "e.g. My Custom Agent",
+ },
},
}
})
@@ -55,10 +74,12 @@ return JSONResponse(content={
### Feature configuration
-- **Simple features**: Boolean values for basic on/off features
-- **Complex features**: Objects with `label`, `default`, and `description` properties
-- **Built-in features**: Standard features like `streaming`, `widget-dashboard-select`, `widget-dashboard-search`
-- **Custom features**: User-defined features with custom behavior
+- **Boolean features**: Simple on/off toggles.
+ - Objects with `label`, `default`, and `description` properties
+- **Text features**: Free-form string input.
+ - Objects with `label`, `type: "text"`, `default`, `description`, and optional `placeholder`
+- **Select features**: Dropdown selection.
+ - Objects with `label`, `type: "select"`, `default`, `description`, and `options` (array of `{label, value}` objects)
### Query flow
@@ -70,9 +91,10 @@ return JSONResponse(content={
### OpenBB AI SDK
-- `QueryRequest.workspace_options`: List of enabled feature names
+- `QueryRequest.workspace_options`: Dictionary of option values keyed by option id
- `message_chunk(text)`: Streams response content with feature-aware messaging
-- Feature checking via simple list membership: `"feature-name" in workspace_options`
+- Boolean features appear as `true` or `false`: `workspace_options["web-search"]`
+- Text and select features appear as string values: `workspace_options["model"]`
## Core logic
@@ -80,22 +102,28 @@ return JSONResponse(content={
from typing import AsyncGenerator
from openbb_ai import message_chunk
from openbb_ai.models import MessageChunkSSE, QueryRequest
-from fastapi.responses import JSONResponse
from sse_starlette.sse import EventSourceResponse
@app.post("/v1/query")
async def query(request: QueryRequest) -> EventSourceResponse:
# Access workspace options from request payload
- workspace_options = getattr(request, "workspace_options", [])
+ # Example: {"web-search": true, "model": "gpt-4o", "agent-name": "My Bot"}
+ workspace_options = getattr(request, "workspace_options", {}) or {}
+
+ # Check boolean features
+ deep_research_enabled = bool(workspace_options.get("deep-research", False))
+ web_search_enabled = bool(workspace_options.get("web-search", True))
- # Check which features are enabled
- deep_research_enabled = "deep-research" in workspace_options
- web_search_enabled = "web-search" in workspace_options
+ # Read text/select feature values
+ model = workspace_options.get("model", "claude-sonnet-4-20250514")
+ agent_name = workspace_options.get("agent-name", "Example Agent")
# Build feature status message
features_msg = (
f"- Deep Research: {'✅ Enabled' if deep_research_enabled else '❌ Disabled'}\n"
- f"- Web Search: {'✅ Enabled' if web_search_enabled else '❌ Disabled'}"
+ f"- Web Search: {'✅ Enabled' if web_search_enabled else '❌ Disabled'}\n"
+ f"- Model: {model}\n"
+ f"- Agent Name: {agent_name}"
)
# Include feature status in system prompt
@@ -103,7 +131,7 @@ async def query(request: QueryRequest) -> EventSourceResponse:
{
"role": "system",
"content": (
- "You are a simple greeting agent.\n"
+ f'Your name is "{agent_name}".\n'
"Greet the user and let them know their current feature settings:\n"
f"{features_msg}\n"
"Keep your response brief and friendly."
@@ -139,36 +167,69 @@ async def query(request: QueryRequest) -> EventSourceResponse:
## Feature types
### Boolean features
-Simple on/off switches in the agent descriptor:
+
+Simple on/off toggles with a label and default state:
+
+```python
+"features": {
+ "deep-research": {
+ "label": "Deep Research",
+ "default": False,
+ "description": "Allows the copilot to do deep research",
+ }
+}
+```
+
+### Text features
+
+Free-form string input with an optional placeholder:
+
```python
"features": {
- "streaming": True,
- "some-feature": False
+ "agent-name": {
+ "label": "Name of Agent",
+ "type": "text",
+ "default": "Example Agent",
+ "description": "Set the name the agent uses to introduce itself.",
+ "placeholder": "e.g. My Custom Agent",
+ }
}
```
-### Complex features
-Rich feature objects with metadata:
+### Select features
+
+Dropdown selection with a list of options:
+
```python
"features": {
- "research-mode": {
- "label": "Research Mode",
- "default": True,
- "description": "Enables comprehensive research capabilities"
+ "model": {
+ "label": "Model",
+ "type": "select",
+ "default": "claude-sonnet-4-20250514",
+ "description": "Select the LLM model to use.",
+ "options": [
+ {"label": "Claude Opus 4", "value": "claude-opus-4-0-20250514"},
+ {"label": "Claude Sonnet 4", "value": "claude-sonnet-4-20250514"},
+ {"label": "GPT-4o", "value": "gpt-4o"},
+ {"label": "GPT-4o mini", "value": "gpt-4o-mini"},
+ ],
}
}
```
-### Conditional behavior
-Adjust agent behavior based on enabled features:
+### Reading feature values
+
+Boolean, text, and select values are sent in `workspace_options` as a dictionary keyed by option id:
+
```python
-workspace_options = getattr(request, "workspace_options", [])
+workspace_options = getattr(request, "workspace_options", {}) or {}
-if "research-mode" in workspace_options:
- # Enable research capabilities
+# Boolean features
+if workspace_options.get("deep-research", False):
+ # Enable deep research capabilities
pass
-if "web-search" in workspace_options:
- # Enable web search functionality
- pass
-```
\ No newline at end of file
+# Text/select features
+model = workspace_options.get("model", "claude-sonnet-4-20250514")
+agent_name = workspace_options.get("agent-name", "Example Agent")
+```
diff --git a/content/workspace/developers/ai-features/user-feedback.md b/content/workspace/developers/ai-features/user-feedback.md
new file mode 100644
index 0000000000..1366858258
--- /dev/null
+++ b/content/workspace/developers/ai-features/user-feedback.md
@@ -0,0 +1,177 @@
+---
+title: User feedback
+sidebar_position: 10
+description: Receive and persist user feedback (thumbs up/down) from the workspace UI
+keywords:
+- feedback
+- thumbs up
+- thumbs down
+- user feedback
+- vote
+- SSE
+---
+
+import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
+
+
+
+Enable thumbs up/down buttons on agent responses and receive user feedback at a dedicated endpoint. This lets agents collect ratings, comments, and tags on their responses for quality monitoring and improvement.
+
+Reference implementation in [this GitHub repository](https://github.com/OpenBB-finance/agents-for-openbb/tree/main/42-vanilla-agent-feedback).
+
+
+
+
+
+
+
+
+
+## Architecture
+
+Declare feedback support in the agent descriptor and expose a POST endpoint to receive votes. When feedback is enabled, the workspace renders thumbs up/down buttons on each agent response.
+
+`agents.json` configuration:
+
+```python
+return JSONResponse(content={
+ "vanilla_agent_feedback": {
+ "name": "Vanilla Agent Feedback",
+ "description": "A vanilla agent that receives and persists user feedback (thumbs up/down).",
+ "endpoints": {
+ "query": "/v1/query",
+ "feedback": "/v1/feedback",
+ },
+ "features": {
+ "streaming": True,
+ "widget-dashboard-select": False,
+ "widget-dashboard-search": False,
+ "feedback": True,
+ },
+ }
+})
+```
+
+- `"feedback": True` in `features` enables the thumbs up/down UI on agent responses
+- `"feedback": "/v1/feedback"` in `endpoints` tells the workspace where to POST feedback data
+
+### Query flow
+
+- User sends a query and the agent streams a normal response
+- After the response, the workspace renders thumbs up/down buttons
+- User clicks a thumb and optionally adds tags or a comment
+- Workspace POSTs a feedback payload to the agent's `/v1/feedback` endpoint
+- Agent persists the feedback (to a file, database, or analytics service)
+
+### OpenBB AI SDK
+
+- `QueryRequest` and `message_chunk(text)` handle the standard query flow
+- The feedback endpoint is a plain FastAPI POST route — no SDK wrapper is required
+- `FeedbackRequest` is a custom Pydantic model defined by the agent
+
+## Core logic
+
+### Feedback model
+
+```python
+from pydantic import BaseModel
+
+class FeedbackRequest(BaseModel):
+ vote: str # "thumbs_up" or "thumbs_down"
+ tags: list[str] = [] # e.g. ["Not factually correct / Hallucinations / Inaccurate"]
+ user_comment: str = "" # Optional free-text comment
+ ai_response: str = "" # The AI response that was rated
+ user_prompt: str = "" # The user's original prompt
+ trace_id: str = "" # Request trace ID for correlation
+```
+
+### Feedback endpoint
+
+```python
+import json
+from datetime import datetime, timezone
+from pathlib import Path
+from fastapi.responses import JSONResponse
+
+FEEDBACK_FILE = Path(__file__).parent.parent / "feedback.json"
+
+@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"})
+```
+
+### Query endpoint
+
+The query endpoint is a standard streaming handler — nothing feedback-specific is required:
+
+```python
+from typing import AsyncGenerator
+import openai
+from openbb_ai import message_chunk
+from openbb_ai.models import MessageChunkSSE, QueryRequest
+from sse_starlette.sse import EventSourceResponse
+
+@app.post("/v1/query")
+async def query(request: QueryRequest) -> EventSourceResponse:
+ openai_messages = [
+ {"role": "system", "content": "You are a helpful financial assistant."}
+ ]
+
+ for message in request.messages:
+ if message.role == "human":
+ openai_messages.append({"role": "user", "content": message.content})
+ elif message.role == "ai" and isinstance(message.content, str):
+ openai_messages.append({"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",
+ )
+```
+
+## Feedback payload
+
+The workspace sends the following JSON to the feedback endpoint:
+
+```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"
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `vote` | string | `"thumbs_up"` or `"thumbs_down"` |
+| `tags` | string[] | Predefined feedback categories selected by the user |
+| `user_comment` | string | Optional free-text comment |
+| `ai_response` | string | The agent response that was rated |
+| `user_prompt` | string | The user's original prompt |
+| `trace_id` | string | Trace ID for correlating feedback with requests |
diff --git a/content/workspace/developers/json-specs/agents-json-reference.md b/content/workspace/developers/json-specs/agents-json-reference.md
index 931fa13846..93b4e1f112 100644
--- a/content/workspace/developers/json-specs/agents-json-reference.md
+++ b/content/workspace/developers/json-specs/agents-json-reference.md
@@ -107,6 +107,8 @@ Configuration object that declares the capabilities that your agent advertises t
See continuation of this section for details on each feature.
+Custom feature controls are also declared in `features`. Use object values for custom toggle, text, and select controls. See [Custom agent features](../ai-features/custom-agent-features) for the control schema and `workspace_options` request payload.
+
#### `streaming`: Streaming responses
The streaming feature enables Server-Sent Events (SSE) for your agent's responses, allowing real-time streaming of content back to users. This provides a better user experience with progressive response rendering. This feature defaults to `True` for all agents even if not explicitly set.
diff --git a/content/workspace/developers/openbb-ai-sdk.md b/content/workspace/developers/openbb-ai-sdk.md
index 98738f77c5..e40f6d5727 100644
--- a/content/workspace/developers/openbb-ai-sdk.md
+++ b/content/workspace/developers/openbb-ai-sdk.md
@@ -106,7 +106,7 @@ if request.workspace_state and request.workspace_state.current_dashboard_info:
- **`urls`** - URLs shared in chat (max 4)
- **`timezone`** - User's browser timezone (e.g., "America/New_York")
- **`api_keys`** - Custom API keys from user
-- **`workspace_options`** - Enabled feature flags (including custom ones)
+- **`workspace_options`** - Workspace option values from AI feature settings. See [Custom agent features](./ai-features/custom-agent-features) for the payload shape.
## Requesting Widget Data
@@ -464,10 +464,12 @@ To see the available models check [https://github.com/OpenBB-finance/openbb-ai/b
- `urls` - URL pages shared in chat for context (maximum 4 URLs allowed)
- `timezone` - User's browser-detected timezone (e.g., "America/New_York", "Europe/London", "UTC")
- `api_keys` - Custom API keys provided by user
-- `workspace_options` - Feature flags and custom options. Standard flags include:
+- `workspace_options` - Workspace option values from AI feature settings. Standard flags include:
- `"widget-dashboard-select"` - Access to user-selected widgets
- `"widget-dashboard-search"` - Access to all dashboard widgets
- - Custom features defined in agent's `agents.json` by the user
+ - Custom features defined in the agent's `agents.json`
+
+See [Custom agent features](./ai-features/custom-agent-features) for the `workspace_options` payload shape.
**`Widget`** - Widget definition representing a data source endpoint
- `uuid` - Unique identifier for this widget instance (UUID format: `550e8400-e29b-41d4-a716-446655440000`)