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.
@@ -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