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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ DATA_FOLDER_PATH=data # The path to the folder that will store the allocation d

# AI configuration
OPENROUTER_API_KEY=
AGENT_MODEL=qwen/qwen3.5-397b-a17b # The model slug used by the agent.

# S3 configuration
S3_ENABLED=false # Set to true to enable S3 storage
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/lint_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ jobs:
run: uv run ruff check .

- name: Check code formatting
run: uv run black --check .
run: uv run ruff format --check .

- name: Type checking
run: uv run ty check .

- name: Run tests
run: uv run pytest
11 changes: 9 additions & 2 deletions llms.md → AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# llms.md
# AGENTS.md

This file provides guidance to AI coding agents when working with code in this repository.
This file is symlinked to `CLAUDE.md`, `GEMINI.md` and `.cursorrules` to be automatically read by AI coding assistants.
Expand Down Expand Up @@ -40,7 +40,7 @@ uv run pytest
Code formatting

```bash
uv run black .
uv run ruff format .
```

Linting
Expand All @@ -49,6 +49,12 @@ Linting
uv run ruff check .
```

Type checking

```bash
uv run ty check .
```

### Dependency Management

- **Always use UV commands** - never edit pyproject.toml directly
Expand Down Expand Up @@ -116,6 +122,7 @@ User Request → FastAPI Endpoint (/v1/query) → Agent Execution Loop → Portf
- `S3_*`: Cloud storage configuration (if S3_ENABLED=true)
- `AGENT_HOST_URL`: Application host URL
- `APP_API_KEY`: API access token
- `AGENT_MODEL`: The model slug used by the agent (optional, defaults to `qwen/qwen3.5-397b-a17b`)

### Optional Configuration

Expand Down
1 change: 0 additions & 1 deletion CLAUDE.md

This file was deleted.

1 change: 0 additions & 1 deletion GEMINI.md

This file was deleted.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ docker run --rm -it --name allocator-bot \

- `AGENT_HOST_URL`: The host URL where the app is running (e.g., `http://localhost:4322`)
- `APP_API_KEY`: Your API key to access the bot
- `AGENT_MODEL`: The model slug used by the agent (optional, defaults to `qwen/qwen3.5-397b-a17b`)
- `OPENROUTER_API_KEY`: Your OpenRouter API key for LLM access
- `FMP_API_KEY`: Your Financial Modeling Prep API key for market data

Expand Down
12 changes: 6 additions & 6 deletions allocator_bot/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,21 @@
)
from magentic.chat_model.openrouter_chat_model import OpenRouterChatModel
from magentic.chat_model.retry_chat_model import RetryChatModel
from openbb_ai.helpers import ( # type: ignore[import-untyped]
from openbb_ai.helpers import (
citations,
cite,
message_chunk,
reasoning_step,
table,
)
from openbb_ai.models import ( # type: ignore[import-untyped]
from openbb_ai.models import (
BaseSSE,
QueryRequest,
Widget,
WidgetParam,
)

from .config import config
from .models import TaskStructure
from .portfolio import prepare_allocation
from .prompts import (
Expand All @@ -43,7 +44,7 @@
DO_I_NEED_TO_ALLOCATE_THE_PORTFOLIO_PROMPT,
model=RetryChatModel(
OpenRouterChatModel(
model="deepseek/deepseek-chat-v3-0324",
model=config.agent_model,
temperature=0.0,
provider_sort="latency",
),
Expand All @@ -57,7 +58,7 @@ async def _need_to_allocate_portfolio(conversation: str) -> bool: ... # type: i
PARSE_USER_MESSAGE_TO_STRUCTURE_THE_TASK,
model=RetryChatModel(
OpenRouterChatModel(
model="deepseek/deepseek-chat-v3-0324",
model=config.agent_model,
temperature=0.0,
provider_sort="latency",
),
Expand All @@ -72,7 +73,7 @@ def make_llm(chat_messages: list) -> Callable:
SystemMessage(SYSTEM_PROMPT),
*chat_messages,
model=OpenRouterChatModel(
model="deepseek/deepseek-chat-v3-0324",
model=config.agent_model,
temperature=0.7,
provider_sort="latency",
),
Expand All @@ -99,7 +100,6 @@ async def execution_loop(request: QueryRequest) -> AsyncGenerator[BaseSSE, None]
user_message_content = await sanitize_message(message.content)
chat_messages.append(UserMessage(content=user_message_content))
if await is_last_message(message, request.messages):

# I intentionally am not using function calling in this example
# because I want all the logic that is under the hood to be exposed
# explicitly so that others can use this code as a reference to learn
Expand Down
10 changes: 5 additions & 5 deletions allocator_bot/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import os
from contextlib import asynccontextmanager

import pandas as pd # type: ignore
import pandas as pd
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.security import OAuth2PasswordBearer
from openbb_ai.models import QueryRequest # type: ignore[import-untyped]
from openbb_ai.models import QueryRequest
from sse_starlette.sse import EventSourceResponse

from .agent import execution_loop
Expand Down Expand Up @@ -52,7 +52,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
]

app.add_middleware(
CORSMiddleware,
CORSMiddleware, # type: ignore
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
Expand Down Expand Up @@ -408,10 +408,10 @@ async def get_task_data(
# Sort by timestamp (newest first)
df = df.sort_values("Timestamp", ascending=False)

return JSONResponse(content={"tasks": df.to_dict(orient="records")})
return JSONResponse(content={"tasks": df.fillna("N/A").to_dict(orient="records")})


@app.post("/v1/query")
@app.post("/v1/query", openapi_extra={"widget_config": {"exclude": True}})
async def query(
request: QueryRequest, token: str = Depends(get_current_user)
) -> EventSourceResponse:
Expand Down
1 change: 1 addition & 0 deletions allocator_bot/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
config = AppConfig(
agent_host_url=os.getenv("AGENT_HOST_URL", ""),
app_api_key=os.getenv("APP_API_KEY", ""),
agent_model=os.getenv("AGENT_MODEL", "qwen/qwen3.5-397b-a17b"),
data_folder_path=os.getenv("DATA_FOLDER_PATH", None),
openrouter_api_key=os.getenv("OPENROUTER_API_KEY", ""),
s3_enabled=os.getenv("S3_ENABLED", "false").lower() == "true",
Expand Down
4 changes: 4 additions & 0 deletions allocator_bot/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ class AppConfig(BaseModel):
description="The host URL and port number where the app is running."
)
app_api_key: str = Field(description="The API key to access the bot.")
agent_model: str = Field(
default="qwen/qwen3.5-397b-a17b",
description="The model slug used by the agent.",
)
openrouter_api_key: str = Field(
description="OpenRouter API key for AI functionality."
)
Expand Down
4 changes: 2 additions & 2 deletions allocator_bot/portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import pandas as pd
from openbb_fmp import FMPEquityHistoricalFetcher
from pypfopt import EfficientFrontier, expected_returns, risk_models # type: ignore
from pypfopt import EfficientFrontier, expected_returns, risk_models

from .config import config

Expand All @@ -24,7 +24,7 @@ async def fetch_historical_prices(
},
credentials={"fmp_api_key": config.fmp_api_key or ""},
)
return pd.DataFrame(p.model_dump() for p in price_data) # type: ignore [union-attr]
return pd.DataFrame(p.model_dump() for p in price_data) # type: ignore


async def optimize_portfolio(
Expand Down
2 changes: 1 addition & 1 deletion allocator_bot/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import string
import time

from openbb_ai.models import LlmMessage # type: ignore[import-untyped]
from openbb_ai.models import LlmMessage


def validate_api_key(token: str, api_key: str) -> bool:
Expand Down
4 changes: 2 additions & 2 deletions allocator_bot/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from datetime import datetime, timedelta, timezone

import aiohttp
import boto3 # type: ignore
from botocore.exceptions import ClientError # type: ignore
import boto3
from botocore.exceptions import ClientError
from openbb_fmp import FMPEquityHistoricalFetcher

from .models import AppConfig
Expand Down
3 changes: 3 additions & 0 deletions k8s/02-configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ data:
# Host URL where the app will be accessible
AGENT_HOST_URL: "http://allocator-bot-service"

# Model slug used by the agent
AGENT_MODEL: "qwen/qwen3.5-397b-a17b"

# Data folder path for storing allocation data
DATA_FOLDER_PATH: "data"

Expand Down
5 changes: 5 additions & 0 deletions k8s/04-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ spec:
configMapKeyRef:
name: allocator-bot-config
key: AGENT_HOST_URL
- name: AGENT_MODEL
valueFrom:
configMapKeyRef:
name: allocator-bot-config
key: AGENT_MODEL
- name: APP_API_KEY
valueFrom:
secretKeyRef:
Expand Down
77 changes: 40 additions & 37 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,62 +1,65 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "allocator-bot"
version = "0.2.0"
version = "0.3.0"
description = "An asset allocation bot for OpenBB that uses PyPortfolioOpt to generate efficient frontier allocations."
authors = [{ name = "Theodore Aptekarev", email = "aptekarev@gmail.com" }]
readme = "README.md"
requires-python = ">=3.10,<3.13"
license = { text = "MIT" }
requires-python = ">=3.10,<3.14"
license = "MIT"
authors = [
{ name = "Theodore Aptekarev", email = "aptekarev@gmail.com" },
]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
]

dependencies = [
"aiohttp>=3.9.0",
"boto3>=1.34.0",
"magentic>=0.40.0",
"openbb-ai>=1.7.4",
"openbb-fmp>=1.3.5",
"openbb-platform-api>=1.1.10",
"pandas>=2.2.3",
"pyportfolioopt>=1.5.6",
"sse-starlette>=2.1.3",
"tabulate>=0.9.0",
"theobb>=1.0.0",
"aiohttp>=3.9.0",
"boto3>=1.34.0",
"magentic>=0.40.0",
"openbb-ai>=1.7.4",
"openbb-core>=1.4.8",
"openbb-fmp>=1.3.5",
"openbb-platform-api>=1.1.13",
"pandas>=2.2.3",
"pyportfolioopt>=1.5.6",
"sse-starlette>=2.1.3",
"tabulate>=0.9.0",
"theobb>=1.0.0",
]

[project.optional-dependencies]
dev = [
"black>=24.4.2",
"pandas-stubs>=2.2.3.250527",
"pytest>=8.4.0",
"ruff>=0.4.4",
"types-boto3>=1.38.32",
"coverage>=7.9.1",
"httpx>=0.28.1",
"pandas-stubs>=2.2.3.250527",
"pytest-asyncio>=1.0.0",
"pytest-cov>=6.2.1",
"pytest>=8.4.0",
"ruff>=0.4.4",
"ty>=0.0.21",
"types-boto3>=1.38.32",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project.scripts]
allocator-bot = "allocator_bot.__main__:main"

[tool.hatch.metadata]
allow-direct-references = true

[tool.hatch.build]
include = ["allocator_bot/**"]

[dependency-groups]
dev = [
"coverage>=7.9.1",
"httpx>=0.28.1",
"pytest-asyncio>=1.0.0",
"pytest-cov>=6.2.1",
]
[tool.hatch.metadata]
allow-direct-references = true

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
addopts = "--cov=allocator_bot --cov-report=term-missing"
filterwarnings = [
"ignore:UserImageMessage is deprecated:DeprecationWarning",
"ignore:Inheritance class ClientSession from ClientSession is discouraged:DeprecationWarning",
]
Loading
Loading