openbb-app 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openbb_app-0.1.3/PKG-INFO +39 -0
- openbb_app-0.1.3/README.md +25 -0
- openbb_app-0.1.3/pyproject.toml +23 -0
- openbb_app-0.1.3/src/openbb_app/__init__.py +0 -0
- openbb_app-0.1.3/src/openbb_app/core/__init__.py +0 -0
- openbb_app-0.1.3/src/openbb_app/core/agent.py +110 -0
- openbb_app-0.1.3/src/openbb_app/core/auth.py +22 -0
- openbb_app-0.1.3/src/openbb_app/core/config.py +14 -0
- openbb_app-0.1.3/src/openbb_app/core/landing.html +29 -0
- openbb_app-0.1.3/src/openbb_app/core/models.py +43 -0
- openbb_app-0.1.3/src/openbb_app/core/plotly_config.py +312 -0
- openbb_app-0.1.3/src/openbb_app/core/registry.py +113 -0
- openbb_app-0.1.3/src/openbb_app/core/session_manager.py +33 -0
- openbb_app-0.1.3/src/openbb_app/core/utils.py +44 -0
- openbb_app-0.1.3/src/openbb_app/main.py +30 -0
- openbb_app-0.1.3/src/openbb_app/py.typed +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: openbb-app
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: Roger Ye
|
|
6
|
+
Author-email: Roger Ye <shugaoye@yahoo.com>
|
|
7
|
+
Requires-Dist: mysharelib>=1.0.4
|
|
8
|
+
Requires-Dist: openbb>=4.7.0
|
|
9
|
+
Requires-Dist: openbb-akshare>=1.0.5
|
|
10
|
+
Requires-Dist: openbb-tushare>=1.0.0
|
|
11
|
+
Requires-Dist: uvicorn>=0.40.0
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# traditional way to start the API directly
|
|
17
|
+
openbb-api --app main.py --exclude '"/api/v1/*"' --reload
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
openbb-api --app main.py --reload
|
|
22
|
+
openbb-api --app src/openbb_app/main.py --reload
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
# using uvx
|
|
26
|
+
You can also run the application using the `uvx` runner that comes with the
|
|
27
|
+
`uv` build system. This lets you invoke the same command from within the
|
|
28
|
+
project environment:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uvx run openbb-api --app src/openbb_app/main.py
|
|
32
|
+
# or simply
|
|
33
|
+
uvx openbb-api --app src/openbb_app/main.py
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Start the virtual environment and run the following command to start the API:
|
|
37
|
+
```shell
|
|
38
|
+
uvicorn openbb_app.main:app
|
|
39
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
```bash
|
|
2
|
+
# traditional way to start the API directly
|
|
3
|
+
openbb-api --app main.py --exclude '"/api/v1/*"' --reload
|
|
4
|
+
```
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
openbb-api --app main.py --reload
|
|
8
|
+
openbb-api --app src/openbb_app/main.py --reload
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
# using uvx
|
|
12
|
+
You can also run the application using the `uvx` runner that comes with the
|
|
13
|
+
`uv` build system. This lets you invoke the same command from within the
|
|
14
|
+
project environment:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uvx run openbb-api --app src/openbb_app/main.py
|
|
18
|
+
# or simply
|
|
19
|
+
uvx openbb-api --app src/openbb_app/main.py
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Start the virtual environment and run the following command to start the API:
|
|
23
|
+
```shell
|
|
24
|
+
uvicorn openbb_app.main:app
|
|
25
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "openbb-app"
|
|
3
|
+
version = "0.1.3"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Roger Ye", email = "shugaoye@yahoo.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"mysharelib>=1.0.4",
|
|
12
|
+
"openbb>=4.7.0",
|
|
13
|
+
"openbb-akshare>=1.0.5",
|
|
14
|
+
"openbb-tushare>=1.0.0",
|
|
15
|
+
"uvicorn>=0.40.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["uv_build>=0.9.7,<0.10.0"]
|
|
20
|
+
build-backend = "uv_build"
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
openbb-tool = "openbb_app.main:start"
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import date
|
|
3
|
+
from typing import AsyncGenerator, Callable
|
|
4
|
+
|
|
5
|
+
from magentic import (
|
|
6
|
+
AssistantMessage,
|
|
7
|
+
AsyncStreamedStr,
|
|
8
|
+
SystemMessage,
|
|
9
|
+
UserMessage,
|
|
10
|
+
chatprompt,
|
|
11
|
+
prompt,
|
|
12
|
+
)
|
|
13
|
+
from magentic.chat_model.openrouter_chat_model import OpenRouterChatModel
|
|
14
|
+
from magentic.chat_model.retry_chat_model import RetryChatModel
|
|
15
|
+
from openbb_ai.helpers import ( # type: ignore[import-untyped]
|
|
16
|
+
citations,
|
|
17
|
+
cite,
|
|
18
|
+
message_chunk,
|
|
19
|
+
reasoning_step,
|
|
20
|
+
table,
|
|
21
|
+
)
|
|
22
|
+
from openbb_ai.models import ( # type: ignore[import-untyped]
|
|
23
|
+
BaseSSE,
|
|
24
|
+
QueryRequest,
|
|
25
|
+
Widget,
|
|
26
|
+
WidgetParam,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from .utils import generate_id, is_last_message, sanitize_message
|
|
30
|
+
|
|
31
|
+
SYSTEM_PROMPT = """
|
|
32
|
+
You are an expert financial advisor with extensive knowledge in investment strategies, portfolio management, and market analysis. Your role is to provide clear, practical, and personalized financial advice to help users make informed investment decisions.
|
|
33
|
+
|
|
34
|
+
Core Responsibilities:
|
|
35
|
+
- Analyze user financial situations and investment goals
|
|
36
|
+
- Provide diversified investment recommendations based on risk tolerance
|
|
37
|
+
- Explain investment concepts in accessible language
|
|
38
|
+
- Suggest portfolio allocation strategies
|
|
39
|
+
- Discuss market trends and their potential impact
|
|
40
|
+
- Recommend investment vehicles (stocks, bonds, ETFs, mutual funds, etc.)
|
|
41
|
+
|
|
42
|
+
Guidelines for Interaction:
|
|
43
|
+
- Always emphasize that investment involves risk and past performance doesn't guarantee future results
|
|
44
|
+
- Ask clarifying questions about financial goals, timeline, and risk tolerance before making recommendations
|
|
45
|
+
- Consider user's age, income, expenses, and financial obligations
|
|
46
|
+
- Promote diversified investment strategies to minimize risk
|
|
47
|
+
- Explain the trade-offs between different investment options
|
|
48
|
+
- Be transparent about potential fees and costs associated with investments
|
|
49
|
+
- Recommend consulting with certified financial professionals for complex situations
|
|
50
|
+
|
|
51
|
+
Communication Style:
|
|
52
|
+
- Use clear, jargon-free language while maintaining professional expertise
|
|
53
|
+
- Provide specific examples when explaining concepts
|
|
54
|
+
- Offer actionable advice with step-by-step guidance
|
|
55
|
+
- Acknowledge limitations of AI-based financial advice
|
|
56
|
+
- Maintain ethical standards and avoid conflicts of interest
|
|
57
|
+
|
|
58
|
+
Important Disclaimers:
|
|
59
|
+
- All investment decisions should be made with appropriate professional consultation
|
|
60
|
+
- Market conditions change rapidly - advice should reflect this uncertainty
|
|
61
|
+
- Never provide real-time market data or specific stock picks without appropriate warnings
|
|
62
|
+
- Always remind users to do their own research before investing
|
|
63
|
+
- Do not provide tax advice - recommend consulting tax professionals
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
logger = logging.getLogger(__name__)
|
|
68
|
+
|
|
69
|
+
def make_llm(chat_messages: list) -> Callable:
|
|
70
|
+
@chatprompt(
|
|
71
|
+
SystemMessage(SYSTEM_PROMPT),
|
|
72
|
+
*chat_messages,
|
|
73
|
+
model=OpenRouterChatModel(
|
|
74
|
+
model="deepseek/deepseek-chat-v3-0324",
|
|
75
|
+
temperature=0.7,
|
|
76
|
+
provider_sort="latency",
|
|
77
|
+
require_parameters=True,
|
|
78
|
+
),
|
|
79
|
+
max_retries=5,
|
|
80
|
+
)
|
|
81
|
+
async def _llm() -> AsyncStreamedStr | str: ... # type: ignore[empty-body]
|
|
82
|
+
|
|
83
|
+
return _llm
|
|
84
|
+
|
|
85
|
+
async def execution_loop(request: QueryRequest) -> AsyncGenerator[BaseSSE, None]:
|
|
86
|
+
"""Process the query and generate responses."""
|
|
87
|
+
|
|
88
|
+
chat_messages: list = []
|
|
89
|
+
citations_list: list = []
|
|
90
|
+
for message in request.messages:
|
|
91
|
+
if message.role == "ai":
|
|
92
|
+
if hasattr(message, "content") and isinstance(message.content, str):
|
|
93
|
+
chat_messages.append(
|
|
94
|
+
AssistantMessage(content=await sanitize_message(message.content))
|
|
95
|
+
)
|
|
96
|
+
elif message.role == "human":
|
|
97
|
+
if hasattr(message, "content") and isinstance(message.content, str):
|
|
98
|
+
user_message_content = await sanitize_message(message.content)
|
|
99
|
+
chat_messages.append(UserMessage(content=user_message_content))
|
|
100
|
+
|
|
101
|
+
_llm = make_llm(chat_messages)
|
|
102
|
+
llm_result = await _llm()
|
|
103
|
+
|
|
104
|
+
if isinstance(llm_result, str):
|
|
105
|
+
yield message_chunk(text=llm_result)
|
|
106
|
+
else:
|
|
107
|
+
async for chunk in llm_result:
|
|
108
|
+
yield message_chunk(text=chunk)
|
|
109
|
+
if len(citations_list) > 0:
|
|
110
|
+
yield citations(citations_list)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from fastapi import HTTPException, status, Depends
|
|
2
|
+
from fastapi.security import OAuth2PasswordBearer
|
|
3
|
+
from openbb_app.core.config import config
|
|
4
|
+
|
|
5
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
|
6
|
+
|
|
7
|
+
def validate_api_key(token: str, api_key: str) -> bool:
|
|
8
|
+
"""Validate API key in header against pre-defined list of keys."""
|
|
9
|
+
if not token:
|
|
10
|
+
return False
|
|
11
|
+
if token.replace("Bearer ", "").strip() == api_key:
|
|
12
|
+
return True
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
async def get_current_user(token: str = Depends(oauth2_scheme)):
|
|
16
|
+
if not validate_api_key(token=token, api_key=config.app_api_key):
|
|
17
|
+
raise HTTPException(
|
|
18
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
19
|
+
detail="Invalid or missing API key",
|
|
20
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
21
|
+
)
|
|
22
|
+
return token
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
|
|
5
|
+
from .models import AppConfig
|
|
6
|
+
|
|
7
|
+
load_dotenv()
|
|
8
|
+
|
|
9
|
+
config = AppConfig(
|
|
10
|
+
agent_host_url=os.getenv("AGENT_HOST_URL", ""),
|
|
11
|
+
app_api_key=os.getenv("APP_API_KEY", ""),
|
|
12
|
+
openrouter_api_key=os.getenv("OPENROUTER_API_KEY", ""),
|
|
13
|
+
fmp_api_key=os.getenv("FMP_API_KEY", None),
|
|
14
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
8
|
+
</head>
|
|
9
|
+
|
|
10
|
+
<body>
|
|
11
|
+
<div class="flex min-h-screen flex-col items-center justify-center bg-white py-6">
|
|
12
|
+
<div style="display: flex; flex-direction: column; align-items: center;"
|
|
13
|
+
class="container max-w-3xl px-4 flex flex-col items-center">
|
|
14
|
+
<div class="flex items-center gap-2 text-xl font-bold text-gray-900 mb-6">
|
|
15
|
+
<span>openbb-hka</span>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="text-center space-y-4 mb-8">
|
|
19
|
+
<h1 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
|
|
20
|
+
OpenBB Workspace app for China and Hong Kong market
|
|
21
|
+
</h1>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<p class="text-sm text-gray-600 text-center mb-6">Created by @MattMaximo, @didier_lopes and @jose-donato</p>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</body>
|
|
28
|
+
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field, field_validator
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AppConfig(BaseModel):
|
|
5
|
+
"""Application configuration loaded from environment variables."""
|
|
6
|
+
|
|
7
|
+
title: str = Field(default="FinApp", description="The title of the app.")
|
|
8
|
+
description: str = Field(
|
|
9
|
+
default="FinApp API for OpenBB Workspace", description="The description of the app."
|
|
10
|
+
)
|
|
11
|
+
agent_host_url: str = Field(
|
|
12
|
+
description="The host URL and port number where the app is running."
|
|
13
|
+
)
|
|
14
|
+
app_api_key: str = Field(description="The API key to access the bot.")
|
|
15
|
+
openrouter_api_key: str = Field(
|
|
16
|
+
description="OpenRouter API key for AI functionality."
|
|
17
|
+
)
|
|
18
|
+
fmp_api_key: str | None = Field(
|
|
19
|
+
default=None, description="Financial Modeling Prep API key for data retrieval."
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
@field_validator(
|
|
23
|
+
"agent_host_url", "app_api_key", "openrouter_api_key", mode="before"
|
|
24
|
+
)
|
|
25
|
+
def validate_required_env_vars(cls, value: str | None, info) -> str | None:
|
|
26
|
+
"""Validate required environment variables.
|
|
27
|
+
|
|
28
|
+
Raises ValueError if any required variable is not set.
|
|
29
|
+
"""
|
|
30
|
+
if not value:
|
|
31
|
+
raise ValueError(f"{info.field_name} environment variable is required.")
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
@field_validator("fmp_api_key")
|
|
35
|
+
def validate_fmp_api_key(cls, value: str | None) -> str | None:
|
|
36
|
+
"""Validate the Financial Modeling Prep API key.
|
|
37
|
+
|
|
38
|
+
Must be set if FMP data retrieval is required.
|
|
39
|
+
Raises ValueError if the key is not valid.
|
|
40
|
+
"""
|
|
41
|
+
if value is None:
|
|
42
|
+
raise ValueError("FMP API key must be set for data retrieval.")
|
|
43
|
+
return value
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plotly config settings for consistent chart behavior.
|
|
3
|
+
|
|
4
|
+
This module provides standardized configuration options for Plotly charts,
|
|
5
|
+
ensuring consistent interactivity, responsiveness, and appearance.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
def create_base_layout(
|
|
9
|
+
x_title: str,
|
|
10
|
+
y_title: str,
|
|
11
|
+
y_dtype: str = ".2s",
|
|
12
|
+
theme: str = "dark"
|
|
13
|
+
):
|
|
14
|
+
"""
|
|
15
|
+
Creates a base layout for a Plotly chart with customizable axis titles and
|
|
16
|
+
y-axis formatting.
|
|
17
|
+
|
|
18
|
+
Parameters:
|
|
19
|
+
- x_title (str): The title for the x-axis. If the title is a date-related
|
|
20
|
+
term, it will be set to None.
|
|
21
|
+
- y_title (str): The title for the y-axis.
|
|
22
|
+
- y_dtype (str): Optional. Specifies the format of the y-axis labels.
|
|
23
|
+
Default is ".2s".
|
|
24
|
+
Available options include:
|
|
25
|
+
- ".2s": Short scale formatting with two significant digits (e.g., 1.2K).
|
|
26
|
+
- ".2f": Fixed-point notation with two decimal places (e.g., 1234.56).
|
|
27
|
+
- ".0f": Fixed-point notation with no decimal places (e.g., 1235).
|
|
28
|
+
- ".0%": Percentage with no decimal places (e.g., 50%).
|
|
29
|
+
- ".2%": Percentage with two decimal places (e.g., 50.00%).
|
|
30
|
+
- "$,.2f": Currency format with two decimal places and comma as
|
|
31
|
+
thousand separator (e.g., $1,234.56).
|
|
32
|
+
- ".2e": Scientific notation with two decimal places (e.g., 1.23e+3).
|
|
33
|
+
- theme (str): Optional. The theme to use, either "light" or "dark".
|
|
34
|
+
Default is "dark".
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
- dict: A dictionary representing the layout configuration for a Plotly chart.
|
|
38
|
+
"""
|
|
39
|
+
# Define colors based on theme
|
|
40
|
+
if theme == "light":
|
|
41
|
+
text_color = "#333333" # Dark gray for light theme
|
|
42
|
+
legend_text_color = "#000000" # Black for legend text in light mode
|
|
43
|
+
grid_color = "rgba(128, 128, 128, 0.2)"
|
|
44
|
+
paper_bgcolor = "rgba(255,255,255,0)" # Transparent white
|
|
45
|
+
plot_bgcolor = "rgba(255,255,255,0)" # Transparent white
|
|
46
|
+
hoverlabel_bgcolor = "black"
|
|
47
|
+
hoverlabel_font_color = "white"
|
|
48
|
+
legend_bgcolor = "rgba(255, 255, 255, 0.9)" # Opaque white background
|
|
49
|
+
legend_bordercolor = "#666666" # Dark gray border
|
|
50
|
+
else: # dark theme (default)
|
|
51
|
+
text_color = "#ffffff" # White for dark theme
|
|
52
|
+
legend_text_color = text_color # Same as text color for dark mode
|
|
53
|
+
grid_color = "rgba(128, 128, 128, 0.2)"
|
|
54
|
+
paper_bgcolor = "rgba(0,0,0,0)" # Transparent black
|
|
55
|
+
plot_bgcolor = "rgba(0,0,0,0)" # Transparent black
|
|
56
|
+
hoverlabel_bgcolor = "white"
|
|
57
|
+
hoverlabel_font_color = "black"
|
|
58
|
+
legend_bgcolor = "rgba(0, 0, 0, 0.7)" # Semi-transparent black
|
|
59
|
+
legend_bordercolor = "#444444" # Light gray border
|
|
60
|
+
|
|
61
|
+
if x_title.lower() in ['date', 'time', 'timestamp', 'datetime']:
|
|
62
|
+
x_title = None
|
|
63
|
+
return dict(
|
|
64
|
+
title=None,
|
|
65
|
+
xaxis=dict(
|
|
66
|
+
title=x_title,
|
|
67
|
+
showgrid=False, # Remove x-axis gridlines
|
|
68
|
+
color=text_color,
|
|
69
|
+
),
|
|
70
|
+
yaxis=dict(
|
|
71
|
+
title=y_title,
|
|
72
|
+
showgrid=True, # Show primary y-axis gridlines
|
|
73
|
+
gridcolor=grid_color,
|
|
74
|
+
color=text_color,
|
|
75
|
+
tickformat=y_dtype
|
|
76
|
+
),
|
|
77
|
+
yaxis2=dict(
|
|
78
|
+
showgrid=False, # Hide secondary y-axis gridlines
|
|
79
|
+
color=text_color,
|
|
80
|
+
),
|
|
81
|
+
legend=dict(
|
|
82
|
+
orientation="h",
|
|
83
|
+
yanchor="bottom",
|
|
84
|
+
y=1.02, # Position above the plot
|
|
85
|
+
xanchor="center",
|
|
86
|
+
x=0.5, # Center the legend
|
|
87
|
+
font=dict(color=legend_text_color), # Use dedicated legend text color
|
|
88
|
+
bgcolor=legend_bgcolor, # Add background color
|
|
89
|
+
bordercolor=legend_bordercolor, # Add border color
|
|
90
|
+
borderwidth=1, # Add border width
|
|
91
|
+
),
|
|
92
|
+
margin=dict(b=0, l=0, r=0, t=0), # Adjust margin for the title
|
|
93
|
+
paper_bgcolor=paper_bgcolor,
|
|
94
|
+
plot_bgcolor=plot_bgcolor,
|
|
95
|
+
font=dict(color=text_color),
|
|
96
|
+
hovermode="x unified", # Put all hover data on the same x-axis
|
|
97
|
+
hoverlabel=dict(
|
|
98
|
+
bgcolor=hoverlabel_bgcolor,
|
|
99
|
+
font_color=hoverlabel_font_color
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_default_config():
|
|
105
|
+
"""
|
|
106
|
+
Returns the default configuration for all Plotly charts in the application.
|
|
107
|
+
|
|
108
|
+
This configuration:
|
|
109
|
+
- Enables responsive behavior for charts
|
|
110
|
+
- Configures the mode bar with appropriate settings
|
|
111
|
+
- Sets up standard interaction modes
|
|
112
|
+
- Defines transition animations
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
dict: A dictionary of Plotly configuration settings
|
|
116
|
+
"""
|
|
117
|
+
return {
|
|
118
|
+
# Display options
|
|
119
|
+
'displayModeBar': True, # Always show the mode bar
|
|
120
|
+
'responsive': True, # Make charts responsive to window size
|
|
121
|
+
'scrollZoom': True, # Enable scroll to zoom
|
|
122
|
+
|
|
123
|
+
# Mode bar configuration
|
|
124
|
+
'modeBarButtonsToRemove': [
|
|
125
|
+
'lasso2d', # Remove lasso selection tool
|
|
126
|
+
'select2d', # Remove box selection tool
|
|
127
|
+
'autoScale2d', # Remove auto scale
|
|
128
|
+
'toggleSpikelines', # Remove spike lines
|
|
129
|
+
'hoverClosestCartesian', # Remove closest point hover
|
|
130
|
+
'hoverCompareCartesian' # Remove compare hover
|
|
131
|
+
],
|
|
132
|
+
'modeBarButtonsToAdd': [
|
|
133
|
+
'drawline',
|
|
134
|
+
'drawcircle',
|
|
135
|
+
'drawrect',
|
|
136
|
+
'eraseshape'
|
|
137
|
+
],
|
|
138
|
+
|
|
139
|
+
# Interaction settings
|
|
140
|
+
'doubleClick': 'reset+autosize', # Double-click to reset view
|
|
141
|
+
'showTips': True, # Show tips for interactions
|
|
142
|
+
|
|
143
|
+
# Other settings
|
|
144
|
+
'watermark': False,
|
|
145
|
+
'staticPlot': False, # Enable interactivity
|
|
146
|
+
'locale': 'en',
|
|
147
|
+
'showAxisDragHandles': True, # Show axis drag handles
|
|
148
|
+
'showAxisRangeEntryBoxes': True, # Show axis range entry boxes
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def get_chart_colors(theme="dark"):
|
|
153
|
+
"""
|
|
154
|
+
Returns standard colors for chart elements based on the theme.
|
|
155
|
+
|
|
156
|
+
Parameters:
|
|
157
|
+
theme (str): The theme to use, either "light" or "dark"
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
dict: A dictionary of color settings for various chart elements
|
|
161
|
+
"""
|
|
162
|
+
if theme == "light":
|
|
163
|
+
return {
|
|
164
|
+
# Main chart line colors
|
|
165
|
+
'text': '#2E5090',
|
|
166
|
+
'main_line': '#2E5090', # Navy blue for light theme
|
|
167
|
+
'positive': '#00AA44', # Forest green for positive values
|
|
168
|
+
'negative': '#CC0000', # Red for negative values
|
|
169
|
+
'neutral': '#3366CC', # Blue for neutral values
|
|
170
|
+
'sma_line': 'black', # SMA line color
|
|
171
|
+
# Additional colors for multiple series
|
|
172
|
+
'secondary': '#8C4646', # Burgundy
|
|
173
|
+
'tertiary': '#5F4B8B', # Muted purple
|
|
174
|
+
"quaternary": "#d3d3d3"
|
|
175
|
+
}
|
|
176
|
+
else: # dark theme (default)
|
|
177
|
+
return {
|
|
178
|
+
# Main chart line colors
|
|
179
|
+
'text': '#FF8000',
|
|
180
|
+
'main_line': '#FF8000', # orange
|
|
181
|
+
'positive': '#00B140', # green
|
|
182
|
+
'negative': '#F4284D', # red
|
|
183
|
+
'neutral': '#2D9BF0', # blue
|
|
184
|
+
'sma_line': 'white', # SMA line color
|
|
185
|
+
# Additional colors for multiple series
|
|
186
|
+
'secondary': '#9E69AF', # purple
|
|
187
|
+
'tertiary': '#00C2DE', # teal
|
|
188
|
+
"quaternary": "#d3d3d3"
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def get_layout_update(theme="dark"):
|
|
193
|
+
"""
|
|
194
|
+
Returns standard layout updates to apply to all charts.
|
|
195
|
+
|
|
196
|
+
This includes:
|
|
197
|
+
- UI revision settings for maintaining state
|
|
198
|
+
- Transition animations
|
|
199
|
+
- Drag mode settings
|
|
200
|
+
- Hover and click behavior
|
|
201
|
+
|
|
202
|
+
Parameters:
|
|
203
|
+
theme (str): The theme to use, either "light" or "dark"
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
dict: A dictionary of layout settings to update Plotly charts
|
|
207
|
+
"""
|
|
208
|
+
# Define color schemes based on theme
|
|
209
|
+
if theme == "light":
|
|
210
|
+
text_color = '#333333'
|
|
211
|
+
grid_color = 'rgba(221, 221, 221, 0.3)' # Very faded grid
|
|
212
|
+
line_color = '#AAAAAA'
|
|
213
|
+
tick_color = '#AAAAAA'
|
|
214
|
+
bg_color = '#ffffff' # More opaque background
|
|
215
|
+
active_color = '#3366CC' # Nice blue color for light theme
|
|
216
|
+
# Black text for better contrast in light mode
|
|
217
|
+
legend_text_color = '#000000'
|
|
218
|
+
# Darker border for better visibility
|
|
219
|
+
legend_border_color = '#ffffff'
|
|
220
|
+
else: # dark theme (default)
|
|
221
|
+
text_color = '#FFFFFF'
|
|
222
|
+
grid_color = 'rgba(51, 51, 51, 0.3)' # Very faded grid
|
|
223
|
+
line_color = '#444444'
|
|
224
|
+
tick_color = '#444444'
|
|
225
|
+
bg_color = '#151518' # More opaque background
|
|
226
|
+
active_color = '#FF8000' # Orange color for dark theme
|
|
227
|
+
legend_text_color = text_color # Use the same text color
|
|
228
|
+
legend_border_color = "#151518" # Use the same border color
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
'uirevision': 'constant', # Maintains view state during updates
|
|
232
|
+
'autosize': True, # Enables auto-sizing for responsive behavior
|
|
233
|
+
'dragmode': 'zoom', # Sets default mode to zoom instead of pan
|
|
234
|
+
'hovermode': 'closest', # Improves hover experience
|
|
235
|
+
'clickmode': 'event', # Makes clicking more responsive
|
|
236
|
+
'margin': {
|
|
237
|
+
't': 50, # Top margin - increase this for more modebar space
|
|
238
|
+
'r': 30, # Right margin
|
|
239
|
+
'b': 40, # Bottom margin
|
|
240
|
+
'l': 40, # Left margin
|
|
241
|
+
'pad': 4 # Padding between the plotting area and the axis lines
|
|
242
|
+
},
|
|
243
|
+
'transition': {
|
|
244
|
+
'duration': 50, # Small transition for smoother feel
|
|
245
|
+
'easing': 'cubic-in-out' # Smooth easing function
|
|
246
|
+
},
|
|
247
|
+
'modebar': {
|
|
248
|
+
'orientation': 'v', # Vertical orientation for modebar
|
|
249
|
+
'activecolor': active_color # Active button color
|
|
250
|
+
},
|
|
251
|
+
'font': {
|
|
252
|
+
'family': 'Arial, sans-serif', # Sans-serif font
|
|
253
|
+
'size': 12,
|
|
254
|
+
'color': text_color # Text color based on theme
|
|
255
|
+
},
|
|
256
|
+
'xaxis': {
|
|
257
|
+
'rangeslider': {'visible': False}, # Disable rangeslider
|
|
258
|
+
'autorange': True, # Enable autorange
|
|
259
|
+
'constrain': 'domain', # Constrain to domain for better zoom
|
|
260
|
+
'showgrid': True, # Show vertical grid lines
|
|
261
|
+
'gridcolor': grid_color, # Very faded grid lines
|
|
262
|
+
'linecolor': line_color, # Axis line color based on theme
|
|
263
|
+
'tickcolor': tick_color, # Tick color based on theme
|
|
264
|
+
'linewidth': 1, # Match y-axis line width
|
|
265
|
+
'mirror': True, # Mirror axis to match y-axis
|
|
266
|
+
'showline': False, # Hide the axis line to remove the box
|
|
267
|
+
'zeroline': False, # Hide zero line to match y-axis
|
|
268
|
+
'ticks': 'outside', # Place ticks outside
|
|
269
|
+
'tickwidth': 1 # Match y-axis tick width
|
|
270
|
+
},
|
|
271
|
+
'yaxis': {
|
|
272
|
+
'autorange': True, # Enable autorange
|
|
273
|
+
'constrain': 'domain', # Constrain to domain
|
|
274
|
+
'fixedrange': False, # Allow y-axis zooming
|
|
275
|
+
'showgrid': True, # Show horizontal grid lines
|
|
276
|
+
'gridcolor': grid_color, # Very faded grid lines
|
|
277
|
+
'linecolor': line_color, # Axis line color based on theme
|
|
278
|
+
'tickcolor': tick_color, # Tick color based on theme
|
|
279
|
+
'linewidth': 1, # Consistent line width
|
|
280
|
+
'mirror': True, # Mirror axis
|
|
281
|
+
'showline': False, # Hide the axis line to remove the box
|
|
282
|
+
'zeroline': False, # Hide zero line
|
|
283
|
+
'ticks': 'outside', # Place ticks outside
|
|
284
|
+
'tickwidth': 1 # Consistent tick width
|
|
285
|
+
},
|
|
286
|
+
'legend': {
|
|
287
|
+
# Legend text color with better contrast
|
|
288
|
+
'font': {'color': legend_text_color},
|
|
289
|
+
'bgcolor': bg_color, # More opaque background
|
|
290
|
+
'bordercolor': legend_border_color, # Better visible border
|
|
291
|
+
'borderwidth': 1 # Add border width for better visibility
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def apply_config_to_figure(figure, theme="dark"):
|
|
297
|
+
"""
|
|
298
|
+
Applies the default configuration and layout updates to a Plotly figure.
|
|
299
|
+
|
|
300
|
+
Parameters:
|
|
301
|
+
figure (plotly.graph_objects.Figure): The Plotly figure to configure
|
|
302
|
+
theme (str): The theme to use, either "light" or "dark"
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
tuple: (figure, config) where figure is the configured Plotly figure
|
|
306
|
+
and config is the configuration dictionary
|
|
307
|
+
"""
|
|
308
|
+
# Apply layout updates with the specified theme
|
|
309
|
+
figure.update_layout(**get_layout_update(theme))
|
|
310
|
+
|
|
311
|
+
# Return both the figure and the config
|
|
312
|
+
return figure
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import asyncio
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
# Initialize empty dictionaries for widgets and templates
|
|
8
|
+
WIDGETS = {}
|
|
9
|
+
TEMPLATES = {}
|
|
10
|
+
|
|
11
|
+
def register_widget(widget_config):
|
|
12
|
+
"""
|
|
13
|
+
Decorator that registers a widget configuration in the WIDGETS dictionary.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
widget_config (dict): The widget configuration to add to the WIDGETS
|
|
17
|
+
dictionary. This should follow the same structure as other entries
|
|
18
|
+
in WIDGETS.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
function: The decorated function.
|
|
22
|
+
"""
|
|
23
|
+
def decorator(func):
|
|
24
|
+
@wraps(func)
|
|
25
|
+
async def async_wrapper(*args, **kwargs):
|
|
26
|
+
# Call the original function
|
|
27
|
+
return await func(*args, **kwargs)
|
|
28
|
+
|
|
29
|
+
@wraps(func)
|
|
30
|
+
def sync_wrapper(*args, **kwargs):
|
|
31
|
+
# Call the original function
|
|
32
|
+
return func(*args, **kwargs)
|
|
33
|
+
|
|
34
|
+
# Extract the endpoint from the widget_config
|
|
35
|
+
endpoint = widget_config.get("endpoint")
|
|
36
|
+
if endpoint:
|
|
37
|
+
# Add an id field to the widget_config if not already present
|
|
38
|
+
if "id" not in widget_config:
|
|
39
|
+
widget_config["id"] = endpoint
|
|
40
|
+
|
|
41
|
+
WIDGETS[endpoint] = widget_config
|
|
42
|
+
|
|
43
|
+
# Return the appropriate wrapper based on whether the function is async
|
|
44
|
+
if asyncio.iscoroutinefunction(func):
|
|
45
|
+
return async_wrapper
|
|
46
|
+
return sync_wrapper
|
|
47
|
+
return decorator
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def add_template(template_name: str):
|
|
51
|
+
"""
|
|
52
|
+
Function that adds a template from a JSON file in the templates directory
|
|
53
|
+
to the TEMPLATES dictionary.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
template_name (str): The name of the template file (without .json
|
|
57
|
+
extension)
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
bool: True if template was successfully added, False otherwise
|
|
61
|
+
"""
|
|
62
|
+
template_path = os.path.join(Path(__file__).parent.parent.resolve(), "templates", f"{template_name}.json")
|
|
63
|
+
|
|
64
|
+
# Check if file exists
|
|
65
|
+
if not os.path.exists(template_path):
|
|
66
|
+
print(f"Template file not found: {template_path}")
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
# Check if JSON is valid
|
|
70
|
+
try:
|
|
71
|
+
with open(template_path, 'r') as f:
|
|
72
|
+
template_data = json.load(f)
|
|
73
|
+
# Register the template in the TEMPLATES dictionary
|
|
74
|
+
TEMPLATES[template_name] = template_data
|
|
75
|
+
return True
|
|
76
|
+
except json.JSONDecodeError as e:
|
|
77
|
+
print(f"Invalid JSON in template {template_name}: {e}")
|
|
78
|
+
return False
|
|
79
|
+
except Exception as e:
|
|
80
|
+
print(f"Error loading template {template_name}: {e}")
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def load_agent_config(template_name: str = "agents"):
|
|
85
|
+
"""
|
|
86
|
+
Function that loads the agent configuration from a JSON file in the templates directory.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
template_name (str): The name of the template file (without .json
|
|
90
|
+
extension)
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
str: JSON string containing the agent configuration
|
|
94
|
+
"""
|
|
95
|
+
template_path = os.path.join(Path(__file__).parent.parent.resolve(), "templates", f"{template_name}.json")
|
|
96
|
+
|
|
97
|
+
# Check if file exists
|
|
98
|
+
if not os.path.exists(template_path):
|
|
99
|
+
print(f"Template file not found: {template_path}")
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
# Check if JSON is valid
|
|
103
|
+
try:
|
|
104
|
+
with open(template_path, 'r') as f:
|
|
105
|
+
template_data = json.load(f)
|
|
106
|
+
# Register the template in the TEMPLATES dictionary
|
|
107
|
+
return template_data
|
|
108
|
+
except json.JSONDecodeError as e:
|
|
109
|
+
print(f"Invalid JSON in template {template_name}: {e}")
|
|
110
|
+
return False
|
|
111
|
+
except Exception as e:
|
|
112
|
+
print(f"Error loading template {template_name}: {e}")
|
|
113
|
+
return False
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
import aiohttp
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
|
|
5
|
+
class SessionManager:
|
|
6
|
+
_instance = None
|
|
7
|
+
_session: Optional[aiohttp.ClientSession] = None
|
|
8
|
+
|
|
9
|
+
def __new__(cls):
|
|
10
|
+
if cls._instance is None:
|
|
11
|
+
cls._instance = super().__new__(cls)
|
|
12
|
+
return cls._instance
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
async def get_session(cls, headers: dict = None) -> aiohttp.ClientSession:
|
|
16
|
+
if cls._session is None or cls._session.closed:
|
|
17
|
+
cls._session = aiohttp.ClientSession(headers=headers)
|
|
18
|
+
return cls._session
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
async def close_session(cls):
|
|
22
|
+
if cls._session and not cls._session.closed:
|
|
23
|
+
await cls._session.close()
|
|
24
|
+
cls._session = None
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
@asynccontextmanager
|
|
28
|
+
async def get_session_context(cls, headers: dict = None):
|
|
29
|
+
session = await cls.get_session(headers)
|
|
30
|
+
try:
|
|
31
|
+
yield session
|
|
32
|
+
finally:
|
|
33
|
+
pass # We don't close the session here as it's reused
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import re
|
|
3
|
+
import string
|
|
4
|
+
import time
|
|
5
|
+
from openbb_ai.models import LlmMessage # type: ignore[import-untyped]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def validate_api_key(token: str, api_key: str) -> bool:
|
|
9
|
+
"""Validate API key in header against pre-defined list of keys."""
|
|
10
|
+
if not token:
|
|
11
|
+
return False
|
|
12
|
+
if token.replace("Bearer ", "").strip() == api_key:
|
|
13
|
+
return True
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def sanitize_message(message: str) -> str:
|
|
18
|
+
"""Sanitize a message by escaping forbidden characters."""
|
|
19
|
+
cleaned_message = re.sub(r"(?<!\{)\{(?!{)", "{{", message)
|
|
20
|
+
cleaned_message = re.sub(r"(?<!\})\}(?!})", "}}", cleaned_message)
|
|
21
|
+
return cleaned_message
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def is_last_message(message: LlmMessage, messages: list[LlmMessage]) -> bool:
|
|
25
|
+
"""Check if the message is the last human message in the conversation."""
|
|
26
|
+
human_messages = [msg for msg in messages if msg.role == "human"]
|
|
27
|
+
return message == human_messages[-1] if human_messages else False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def generate_id(length: int = 2) -> str:
|
|
31
|
+
"""Generate a unique ID with a total length of 4 characters."""
|
|
32
|
+
timestamp = int(time.time() * 1000) % 1000
|
|
33
|
+
|
|
34
|
+
base36_chars = string.digits + string.ascii_lowercase
|
|
35
|
+
|
|
36
|
+
def to_base36(num):
|
|
37
|
+
result = ""
|
|
38
|
+
while num > 0:
|
|
39
|
+
result = base36_chars[num % 36] + result
|
|
40
|
+
num //= 36
|
|
41
|
+
return result.zfill(2)
|
|
42
|
+
|
|
43
|
+
random_suffix = "".join(random.choices(base36_chars, k=length))
|
|
44
|
+
return to_base36(timestamp) + random_suffix
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import uvicorn
|
|
2
|
+
from openbb_platform_api.main import app
|
|
3
|
+
import logging
|
|
4
|
+
from mysharelib.tools import setup_logger
|
|
5
|
+
|
|
6
|
+
setup_logger(__name__)
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
@app.get("/health")
|
|
10
|
+
def health_check():
|
|
11
|
+
"""Health check endpoint for monitoring"""
|
|
12
|
+
return {"status": "healthy"}
|
|
13
|
+
|
|
14
|
+
# 2. The CLI Entry Point
|
|
15
|
+
def start():
|
|
16
|
+
"""
|
|
17
|
+
This function is what 'uvx' or 'openbb-tool' will execute.
|
|
18
|
+
We point uvicorn to the string 'openbb_app.main:app'
|
|
19
|
+
so it can find the FastAPI instance.
|
|
20
|
+
"""
|
|
21
|
+
print("🚀 Starting OpenBB Backend on http://0.0.0.0:8000")
|
|
22
|
+
uvicorn.run(
|
|
23
|
+
"openbb_app.main:app",
|
|
24
|
+
host="0.0.0.0",
|
|
25
|
+
port=8000,
|
|
26
|
+
reload=False # Set to False for production/tool use
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
start()
|
|
File without changes
|