agent: look at current info, its aUEC, feat: pull up notifcations.
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
OLLAMA_BASE_URL=http://localhost:11434
|
OLLAMA_BASE_URL=http://localhost:11434
|
||||||
OLLAMA_MODEL=qwen3.5:9b
|
OLLAMA_MODEL=qwen3.5:9b
|
||||||
|
OLLAMA_NUM_CTX=64000
|
||||||
UEX_BASE_URL=https://api.uexcorp.space/2.0
|
UEX_BASE_URL=https://api.uexcorp.space/2.0
|
||||||
UEX_SECRET_KEY=
|
UEX_SECRET_KEY=
|
||||||
UEX_BEARER_TOKEN=
|
UEX_BEARER_TOKEN=
|
||||||
TRADERAI_USER_NAME=
|
TRADERAI_USER_NAME=
|
||||||
TRADERAI_MEMORY_PATH=data/traderai.sqlite3
|
TRADERAI_MEMORY_PATH=data/traderai.sqlite3
|
||||||
|
UEX_NOTIFICATION_POLL_SECONDS=60
|
||||||
REQUIRE_WRITE_APPROVAL=true
|
REQUIRE_WRITE_APPROVAL=true
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ Local Ollama-powered chat for UEX marketplace workflows.
|
|||||||
|
|
||||||
## What It Does
|
## What It Does
|
||||||
|
|
||||||
- Searches active UEX marketplace listings through `GET /marketplace_listings/`.
|
- Searches active/current UEX marketplace listings through `GET /marketplace_listings/`.
|
||||||
- Reads authenticated marketplace negotiations and negotiation messages when `UEX_SECRET_KEY` or `UEX_BEARER_TOKEN` is set.
|
- Reads authenticated marketplace negotiations and negotiation messages when `UEX_SECRET_KEY` or `UEX_BEARER_TOKEN` is set.
|
||||||
- Drafts negotiation messages and marketplace listings as pending actions.
|
- Drafts negotiation messages and marketplace listings as pending actions.
|
||||||
- Requires browser approval before sending authenticated write requests to UEX.
|
- Requires browser approval before sending authenticated write requests to UEX.
|
||||||
- Maintains local SQLite memory with searchable recall for user facts, preferences, and prior chat context.
|
- Maintains local SQLite memory with searchable recall for user facts, preferences, and prior chat context.
|
||||||
- Can create one-time or recurring wake jobs that prompt the assistant later and surface the result in the UI.
|
- Can create one-time or recurring wake jobs that prompt the assistant later and surface the result in the UI.
|
||||||
- Loads the configured UEX user profile from `GET /user` so the assistant knows the current account username, display name, timezone, language, and marketplace-relevant profile details.
|
- Loads the configured UEX user profile from `GET /user` so the assistant knows the current account username, display name, timezone, language, and marketplace-relevant profile details.
|
||||||
|
- Polls authenticated `GET /user_notifications` for unread UEX notifications and surfaces new pending alerts in the chat notification queue.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
@@ -33,11 +34,11 @@ Local Ollama-powered chat for UEX marketplace workflows.
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
Ollama runs locally at `http://localhost:11434` by default. This app talks to Ollama's native chat API with tool schemas, then executes approved UEX calls in the FastAPI backend.
|
Ollama runs locally at `http://localhost:11434` by default. This app talks to Ollama's native chat API with tool schemas, then executes approved UEX calls in the FastAPI backend. `OLLAMA_NUM_CTX` controls the per-request Ollama context window; `64000` is the default because Ollama recommends at least 64k tokens for agent-style workflows when hardware allows it.
|
||||||
|
|
||||||
UEX marketplace posting and negotiation messages are guarded because they are account-affecting write actions. The model can draft them, but the UI approval button performs the final API call.
|
UEX marketplace posting and negotiation messages are guarded because they are account-affecting write actions. The model can draft them, but the UI approval button performs the final API call.
|
||||||
|
|
||||||
The assistant gets runtime context on every chat: current date/time, authenticated UEX identity when credentials are configured, remembered user profile, last interaction time, relevant memories, and recent conversation excerpts. Memory is stored locally at `TRADERAI_MEMORY_PATH`.
|
The assistant gets runtime context on every chat: current date/time, authenticated UEX identity when credentials are configured, remembered user profile, last interaction time, relevant memories, and recent conversation excerpts. It is instructed to prefer open/current marketplace data, avoid historical sale information unless explicitly requested, and treat UEX prices as in-game aUEC/UEC credits rather than real-world dollars. Memory is stored locally at `TRADERAI_MEMORY_PATH`.
|
||||||
|
|
||||||
Wake jobs can be created from chat, for example:
|
Wake jobs can be created from chat, for example:
|
||||||
|
|
||||||
@@ -53,6 +54,8 @@ Every day at 9 AM, wake up and check whether I have marketplace followups.
|
|||||||
|
|
||||||
The scheduler accepts one-time ISO datetimes and five-field cron expressions through the `schedule_wake_job` tool. When a wake job fires, the assistant receives context like the current time and last interaction time, then places its response into the UI notification queue.
|
The scheduler accepts one-time ISO datetimes and five-field cron expressions through the `schedule_wake_job` tool. When a wake job fires, the assistant receives context like the current time and last interaction time, then places its response into the UI notification queue.
|
||||||
|
|
||||||
|
UEX notifications are checked every `UEX_NOTIFICATION_POLL_SECONDS` seconds by default. New unread notifications are deduplicated locally, then displayed in the chat through the same notification queue used by wake jobs.
|
||||||
|
|
||||||
## Sources Used
|
## Sources Used
|
||||||
|
|
||||||
- UEX SwaggerHub OpenAPI v2.1: https://app.swaggerhub.com/apis-docs/dolejska-daniel/UEX-API/v2.1
|
- UEX SwaggerHub OpenAPI v2.1: https://app.swaggerhub.com/apis-docs/dolejska-daniel/UEX-API/v2.1
|
||||||
@@ -60,6 +63,7 @@ The scheduler accepts one-time ISO datetimes and five-field cron expressions thr
|
|||||||
- UEX negotiation message docs: https://uexcorp.space/api/documentation/id/post_marketplace_negotiations_messages/?is_kiosk=1
|
- UEX negotiation message docs: https://uexcorp.space/api/documentation/id/post_marketplace_negotiations_messages/?is_kiosk=1
|
||||||
- Ollama tool calling docs: https://docs.ollama.com/capabilities/tool-calling
|
- Ollama tool calling docs: https://docs.ollama.com/capabilities/tool-calling
|
||||||
- Ollama API streaming/tool-call reference: https://github.com/ollama/ollama/blob/main/docs/api.md
|
- Ollama API streaming/tool-call reference: https://github.com/ollama/ollama/blob/main/docs/api.md
|
||||||
|
- Ollama context length docs: https://docs.ollama.com/context-length
|
||||||
- SQLite FTS5 docs: https://www.sqlite.org/fts5.html
|
- SQLite FTS5 docs: https://www.sqlite.org/fts5.html
|
||||||
- APScheduler AsyncIO scheduler docs: https://apscheduler.readthedocs.io/en/stable/modules/schedulers/asyncio.html
|
- APScheduler AsyncIO scheduler docs: https://apscheduler.readthedocs.io/en/stable/modules/schedulers/asyncio.html
|
||||||
- Letta/MemGPT memory hierarchy background: https://docs.letta.com/concepts/letta
|
- Letta/MemGPT memory hierarchy background: https://docs.letta.com/concepts/letta
|
||||||
|
|||||||
+14
-1
@@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from traderai.agent import OllamaAgent
|
from traderai.agent import OllamaAgent, SYSTEM_PROMPT
|
||||||
from traderai.memory import MemoryStore
|
from traderai.memory import MemoryStore
|
||||||
|
|
||||||
|
|
||||||
@@ -77,3 +77,16 @@ def test_stream_metrics_include_reading_and_writing_rates():
|
|||||||
assert metrics["reading_tokens_per_second"] == 10
|
assert metrics["reading_tokens_per_second"] == 10
|
||||||
assert metrics["writing_tokens"] == 30
|
assert metrics["writing_tokens"] == 30
|
||||||
assert metrics["writing_tokens_per_second"] == 10
|
assert metrics["writing_tokens_per_second"] == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_system_prompt_prefers_current_marketplace_data():
|
||||||
|
assert "open/current" in SYSTEM_PROMPT
|
||||||
|
assert "Do not use historical sale data" in SYSTEM_PROMPT
|
||||||
|
assert "aUEC/UEC credits" in SYSTEM_PROMPT
|
||||||
|
assert "never real-world dollars" in SYSTEM_PROMPT
|
||||||
|
|
||||||
|
|
||||||
|
def test_ollama_options_include_num_ctx():
|
||||||
|
agent = OllamaAgent("http://127.0.0.1:1", "missing-model", EmptyTools(), num_ctx=64000)
|
||||||
|
|
||||||
|
assert agent._ollama_options() == {"num_ctx": 64000}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from traderai.memory import MemoryStore
|
||||||
|
from traderai.scheduler import WakeScheduler
|
||||||
|
|
||||||
|
|
||||||
|
class FakeUEXNotifications:
|
||||||
|
def __init__(self):
|
||||||
|
self.calls = 0
|
||||||
|
|
||||||
|
async def get_user_notifications(self):
|
||||||
|
self.calls += 1
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"notifications": [
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"message": "A buyer replied to your listing.",
|
||||||
|
"redir": "/marketplace/negotiations/abc",
|
||||||
|
"code": "negotiation_reply",
|
||||||
|
"date_added": 123,
|
||||||
|
"date_read": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"message": "Already read.",
|
||||||
|
"date_added": 122,
|
||||||
|
"date_read": 123,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_poll_uex_notifications_adds_unread_once(tmp_path):
|
||||||
|
memory = MemoryStore(str(tmp_path / "memory.sqlite3"))
|
||||||
|
scheduler = WakeScheduler(memory)
|
||||||
|
scheduler.bind_uex_notifications(FakeUEXNotifications())
|
||||||
|
|
||||||
|
first = await scheduler.poll_uex_notifications()
|
||||||
|
second = await scheduler.poll_uex_notifications()
|
||||||
|
outbox = memory.inspect()["outbox"]
|
||||||
|
|
||||||
|
assert len(first) == 1
|
||||||
|
assert second == []
|
||||||
|
assert len(outbox) == 1
|
||||||
|
assert "A buyer replied to your listing." in outbox[0]["content"]
|
||||||
@@ -81,3 +81,16 @@ async def test_uex_client_get_user_normalizes_user_payload():
|
|||||||
result = await client.get_user(authenticated=True)
|
result = await client.get_user(authenticated=True)
|
||||||
|
|
||||||
assert result == {"status": "ok", "user": {"username": "pilot_hudson"}}
|
assert result == {"status": "ok", "user": {"username": "pilot_hudson"}}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@respx.mock
|
||||||
|
async def test_uex_client_get_user_notifications_normalizes_payload():
|
||||||
|
respx.get("https://api.uexcorp.space/2.0/user_notifications/").mock(
|
||||||
|
return_value=Response(200, json={"status": "ok", "data": {"id": 7, "message": "Reply waiting", "date_read": 0}})
|
||||||
|
)
|
||||||
|
client = UEXClient("https://api.uexcorp.space/2.0", bearer_token="bearer")
|
||||||
|
|
||||||
|
result = await client.get_user_notifications()
|
||||||
|
|
||||||
|
assert result == {"status": "ok", "notifications": [{"id": 7, "message": "Reply waiting", "date_read": 0}]}
|
||||||
|
|||||||
+13
-1
@@ -12,7 +12,9 @@ from traderai.tools import ToolRegistry
|
|||||||
|
|
||||||
|
|
||||||
SYSTEM_PROMPT = """You are TraderAI, a local assistant for UEX marketplace work.
|
SYSTEM_PROMPT = """You are TraderAI, a local assistant for UEX marketplace work.
|
||||||
Use tools when the user asks about listings, negotiations, messages, offers, or posting ads.
|
Use tools when the user asks about open/current listings, active negotiations, unread notifications, messages, offers, or posting ads.
|
||||||
|
Prefer open and current UEX marketplace information. Do not use historical sale data, completed sale records, or sale/average-history information unless the user explicitly asks for historical sales.
|
||||||
|
Treat UEX marketplace prices as in-game aUEC/UEC credits, never real-world dollars, unless the user explicitly says otherwise.
|
||||||
For marketplace writes, draft the exact pending action and tell the user what will be sent; never claim it was sent until approval succeeds.
|
For marketplace writes, draft the exact pending action and tell the user what will be sent; never claim it was sent until approval succeeds.
|
||||||
Keep prices, listing ids, slugs, users, and UEX status codes precise. If data is missing, say what you need next."""
|
Keep prices, listing ids, slugs, users, and UEX status codes precise. If data is missing, say what you need next."""
|
||||||
|
|
||||||
@@ -25,12 +27,14 @@ class OllamaAgent:
|
|||||||
tools: ToolRegistry,
|
tools: ToolRegistry,
|
||||||
memory: MemoryStore | None = None,
|
memory: MemoryStore | None = None,
|
||||||
user_name: str | None = None,
|
user_name: str | None = None,
|
||||||
|
num_ctx: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.base_url = base_url.rstrip("/")
|
self.base_url = base_url.rstrip("/")
|
||||||
self.model = model
|
self.model = model
|
||||||
self.tools = tools
|
self.tools = tools
|
||||||
self.memory = memory
|
self.memory = memory
|
||||||
self.user_name = user_name
|
self.user_name = user_name
|
||||||
|
self.num_ctx = num_ctx
|
||||||
self.messages: list[dict[str, Any]] = [{"role": "system", "content": SYSTEM_PROMPT}]
|
self.messages: list[dict[str, Any]] = [{"role": "system", "content": SYSTEM_PROMPT}]
|
||||||
|
|
||||||
async def health(self) -> dict[str, Any]:
|
async def health(self) -> dict[str, Any]:
|
||||||
@@ -165,6 +169,7 @@ class OllamaAgent:
|
|||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": self._messages_with_context(query, previous_interaction=previous_interaction),
|
"messages": self._messages_with_context(query, previous_interaction=previous_interaction),
|
||||||
"tools": self.tools.schemas,
|
"tools": self.tools.schemas,
|
||||||
|
"options": self._ollama_options(),
|
||||||
"stream": False,
|
"stream": False,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -184,6 +189,7 @@ class OllamaAgent:
|
|||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": self._messages_with_context(query, previous_interaction=previous_interaction),
|
"messages": self._messages_with_context(query, previous_interaction=previous_interaction),
|
||||||
"tools": self.tools.schemas,
|
"tools": self.tools.schemas,
|
||||||
|
"options": self._ollama_options(),
|
||||||
"stream": True,
|
"stream": True,
|
||||||
},
|
},
|
||||||
) as response:
|
) as response:
|
||||||
@@ -258,6 +264,11 @@ class OllamaAgent:
|
|||||||
for action in self.tools.pending_actions.values()
|
for action in self.tools.pending_actions.values()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def _ollama_options(self) -> dict[str, Any]:
|
||||||
|
if not self.num_ctx:
|
||||||
|
return {}
|
||||||
|
return {"num_ctx": self.num_ctx}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _tool_status(name: str) -> str:
|
def _tool_status(name: str) -> str:
|
||||||
labels = {
|
labels = {
|
||||||
@@ -267,6 +278,7 @@ class OllamaAgent:
|
|||||||
"get_negotiation_messages": "Reading negotiation messages",
|
"get_negotiation_messages": "Reading negotiation messages",
|
||||||
"draft_negotiation_message": "Drafting message for approval",
|
"draft_negotiation_message": "Drafting message for approval",
|
||||||
"draft_marketplace_listing": "Drafting listing for approval",
|
"draft_marketplace_listing": "Drafting listing for approval",
|
||||||
|
"check_uex_notifications": "Checking UEX notifications",
|
||||||
}
|
}
|
||||||
return labels.get(name, f"Running {name}")
|
return labels.get(name, f"Running {name}")
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
ollama_base_url: str = "http://localhost:11434"
|
ollama_base_url: str = "http://localhost:11434"
|
||||||
ollama_model: str = "qwen3.5:9b"
|
ollama_model: str = "qwen3.5:9b"
|
||||||
|
ollama_num_ctx: int = 64000
|
||||||
uex_base_url: str = "https://api.uexcorp.space/2.0"
|
uex_base_url: str = "https://api.uexcorp.space/2.0"
|
||||||
uex_secret_key: str | None = Field(default=None)
|
uex_secret_key: str | None = Field(default=None)
|
||||||
uex_bearer_token: str | None = Field(default=None)
|
uex_bearer_token: str | None = Field(default=None)
|
||||||
traderai_user_name: str | None = Field(default=None)
|
traderai_user_name: str | None = Field(default=None)
|
||||||
traderai_memory_path: str = "data/traderai.sqlite3"
|
traderai_memory_path: str = "data/traderai.sqlite3"
|
||||||
|
uex_notification_poll_seconds: int = 60
|
||||||
require_write_approval: bool = True
|
require_write_approval: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,23 +7,34 @@ from uuid import uuid4
|
|||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
from apscheduler.triggers.cron import CronTrigger
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
from apscheduler.triggers.date import DateTrigger
|
from apscheduler.triggers.date import DateTrigger
|
||||||
|
from apscheduler.triggers.interval import IntervalTrigger
|
||||||
from tzlocal import get_localzone
|
from tzlocal import get_localzone
|
||||||
|
|
||||||
from traderai.memory import MemoryStore, iso_now, time_since
|
from traderai.memory import MemoryStore, iso_now, time_since
|
||||||
|
|
||||||
|
|
||||||
|
UEX_NOTIFICATION_JOB_ID = "uex-notification-poll"
|
||||||
|
|
||||||
|
|
||||||
class WakeScheduler:
|
class WakeScheduler:
|
||||||
def __init__(self, memory: MemoryStore) -> None:
|
def __init__(self, memory: MemoryStore) -> None:
|
||||||
self.memory = memory
|
self.memory = memory
|
||||||
self.scheduler = AsyncIOScheduler(timezone=get_localzone())
|
self.scheduler = AsyncIOScheduler(timezone=get_localzone())
|
||||||
self.agent = None
|
self.agent = None
|
||||||
|
self.uex = None
|
||||||
|
self.notification_poll_seconds = 60
|
||||||
|
|
||||||
def bind_agent(self, agent: Any) -> None:
|
def bind_agent(self, agent: Any) -> None:
|
||||||
self.agent = agent
|
self.agent = agent
|
||||||
|
|
||||||
|
def bind_uex_notifications(self, uex: Any, poll_seconds: int = 60) -> None:
|
||||||
|
self.uex = uex
|
||||||
|
self.notification_poll_seconds = max(15, poll_seconds)
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
if not self.scheduler.running:
|
if not self.scheduler.running:
|
||||||
self.scheduler.start()
|
self.scheduler.start()
|
||||||
|
self._schedule_notification_poll()
|
||||||
for job in self.memory.list_jobs():
|
for job in self.memory.list_jobs():
|
||||||
self._schedule_existing(job)
|
self._schedule_existing(job)
|
||||||
|
|
||||||
@@ -77,3 +88,59 @@ class WakeScheduler:
|
|||||||
text = await self.agent.generate_wake_response(wake_message)
|
text = await self.agent.generate_wake_response(wake_message)
|
||||||
self.memory.add_outbox(text)
|
self.memory.add_outbox(text)
|
||||||
self.memory.mark_job_run(job_id)
|
self.memory.mark_job_run(job_id)
|
||||||
|
|
||||||
|
def _schedule_notification_poll(self) -> None:
|
||||||
|
if self.uex is None:
|
||||||
|
return
|
||||||
|
self.scheduler.add_job(
|
||||||
|
self.poll_uex_notifications,
|
||||||
|
trigger=IntervalTrigger(seconds=self.notification_poll_seconds),
|
||||||
|
id=UEX_NOTIFICATION_JOB_ID,
|
||||||
|
replace_existing=True,
|
||||||
|
next_run_time=datetime.now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def poll_uex_notifications(self) -> list[dict[str, Any]]:
|
||||||
|
if self.uex is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
response = await self.uex.get_user_notifications()
|
||||||
|
notifications = response.get("notifications") or []
|
||||||
|
pending = [item for item in notifications if not item.get("date_read")]
|
||||||
|
profile = self.memory.get_profile()
|
||||||
|
seen = set(profile.get("uex_seen_notification_keys") or [])
|
||||||
|
new_pending = [item for item in pending if self._notification_key(item) not in seen]
|
||||||
|
|
||||||
|
if new_pending:
|
||||||
|
for item in new_pending:
|
||||||
|
self.memory.add_outbox(self._notification_text(item))
|
||||||
|
seen.update(self._notification_key(item) for item in new_pending)
|
||||||
|
self.memory.set_profile("uex_seen_notification_keys", sorted(seen))
|
||||||
|
self.memory.set_profile("uex_last_notification_check", iso_now())
|
||||||
|
elif notifications:
|
||||||
|
seen.update(self._notification_key(item) for item in pending)
|
||||||
|
self.memory.set_profile("uex_seen_notification_keys", sorted(seen))
|
||||||
|
self.memory.set_profile("uex_last_notification_check", iso_now())
|
||||||
|
|
||||||
|
return new_pending
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _notification_key(item: dict[str, Any]) -> str:
|
||||||
|
for key in ("code", "id"):
|
||||||
|
value = item.get(key)
|
||||||
|
if value not in (None, ""):
|
||||||
|
return f"{key}:{value}"
|
||||||
|
return f"notification:{item.get('date_added')}:{item.get('message')}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _notification_text(item: dict[str, Any]) -> str:
|
||||||
|
message = item.get("message") or "You have a pending UEX notification."
|
||||||
|
redir = item.get("redir")
|
||||||
|
code = item.get("code")
|
||||||
|
details = []
|
||||||
|
if code:
|
||||||
|
details.append(f"code `{code}`")
|
||||||
|
if redir:
|
||||||
|
details.append(f"path `{redir}`")
|
||||||
|
suffix = f" ({', '.join(details)})" if details else ""
|
||||||
|
return f"UEX notification: {message}{suffix}"
|
||||||
|
|||||||
+9
-1
@@ -35,8 +35,16 @@ def create_app() -> FastAPI:
|
|||||||
scheduler = WakeScheduler(memory)
|
scheduler = WakeScheduler(memory)
|
||||||
uex = UEXClient(settings.uex_base_url, settings.uex_secret_key, settings.uex_bearer_token)
|
uex = UEXClient(settings.uex_base_url, settings.uex_secret_key, settings.uex_bearer_token)
|
||||||
tools = ToolRegistry(uex, settings.require_write_approval, memory=memory, scheduler=scheduler)
|
tools = ToolRegistry(uex, settings.require_write_approval, memory=memory, scheduler=scheduler)
|
||||||
agent = OllamaAgent(settings.ollama_base_url, settings.ollama_model, tools, memory=memory, user_name=settings.traderai_user_name)
|
agent = OllamaAgent(
|
||||||
|
settings.ollama_base_url,
|
||||||
|
settings.ollama_model,
|
||||||
|
tools,
|
||||||
|
memory=memory,
|
||||||
|
user_name=settings.traderai_user_name,
|
||||||
|
num_ctx=settings.ollama_num_ctx,
|
||||||
|
)
|
||||||
scheduler.bind_agent(agent)
|
scheduler.bind_agent(agent)
|
||||||
|
scheduler.bind_uex_notifications(uex, settings.uex_notification_poll_seconds)
|
||||||
|
|
||||||
app = FastAPI(title="TraderAI")
|
app = FastAPI(title="TraderAI")
|
||||||
static_dir = Path(__file__).resolve().parent.parent / "web"
|
static_dir = Path(__file__).resolve().parent.parent / "web"
|
||||||
|
|||||||
+17
-2
@@ -44,6 +44,7 @@ class ToolRegistry:
|
|||||||
"recall_memory": self.recall_memory,
|
"recall_memory": self.recall_memory,
|
||||||
"schedule_wake_job": self.schedule_wake_job,
|
"schedule_wake_job": self.schedule_wake_job,
|
||||||
"list_wake_jobs": self.list_wake_jobs,
|
"list_wake_jobs": self.list_wake_jobs,
|
||||||
|
"check_uex_notifications": self.check_uex_notifications,
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -53,7 +54,7 @@ class ToolRegistry:
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "search_marketplace_listings",
|
"name": "search_marketplace_listings",
|
||||||
"description": "Search active UEX marketplace listings. UEX returns up to 100 active listings; filters are applied locally.",
|
"description": "Search active/current UEX marketplace listings only. Prices are in-game aUEC/UEC credits, not real-world dollars. Do not use this as historical sale or completed-sale information. UEX returns up to 100 active listings; filters are applied locally.",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -133,7 +134,7 @@ class ToolRegistry:
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "draft_marketplace_listing",
|
"name": "draft_marketplace_listing",
|
||||||
"description": "Draft a new UEX marketplace listing. This creates a pending action that must be approved before posting.",
|
"description": "Draft a new UEX marketplace listing. Listing prices are in-game aUEC/UEC credits, not real-world dollars. This creates a pending action that must be approved before posting.",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["id_category", "operation", "type", "unit", "title", "description", "price", "currency", "language"],
|
"required": ["id_category", "operation", "type", "unit", "title", "description", "price", "currency", "language"],
|
||||||
@@ -215,6 +216,14 @@ class ToolRegistry:
|
|||||||
"parameters": {"type": "object", "properties": {}},
|
"parameters": {"type": "object", "properties": {}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "check_uex_notifications",
|
||||||
|
"description": "Check authenticated UEX user notifications and return unread pending notifications.",
|
||||||
|
"parameters": {"type": "object", "properties": {}},
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
async def execute(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
async def execute(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||||
@@ -324,6 +333,12 @@ class ToolRegistry:
|
|||||||
return {"error": "Scheduler is not configured."}
|
return {"error": "Scheduler is not configured."}
|
||||||
return {"scheduled_jobs": self.scheduler.list_jobs()}
|
return {"scheduled_jobs": self.scheduler.list_jobs()}
|
||||||
|
|
||||||
|
async def check_uex_notifications(self) -> dict[str, Any]:
|
||||||
|
response = await self.uex.get_user_notifications()
|
||||||
|
notifications = response.get("notifications") or []
|
||||||
|
pending = [item for item in notifications if not item.get("date_read")]
|
||||||
|
return {"count": len(pending), "notifications": pending}
|
||||||
|
|
||||||
def _pending(self, label: str, endpoint: str, payload: dict[str, Any]) -> dict[str, Any]:
|
def _pending(self, label: str, endpoint: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
action_id = str(uuid.uuid4())
|
action_id = str(uuid.uuid4())
|
||||||
payload = {key: value for key, value in payload.items() if value is not None}
|
payload = {key: value for key, value in payload.items() if value is not None}
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ class UEXClient:
|
|||||||
data = data[0] if data else None
|
data = data[0] if data else None
|
||||||
return {"status": body.get("status"), "user": data}
|
return {"status": body.get("status"), "user": data}
|
||||||
|
|
||||||
|
async def get_user_notifications(self) -> dict[str, Any]:
|
||||||
|
body = await self.get("user_notifications", authenticated=True)
|
||||||
|
data = body.get("data") or []
|
||||||
|
if isinstance(data, dict):
|
||||||
|
data = [data]
|
||||||
|
return {"status": body.get("status"), "notifications": data}
|
||||||
|
|
||||||
async def post(self, path: str, payload: dict[str, Any], authenticated: bool = True) -> dict[str, Any]:
|
async def post(self, path: str, payload: dict[str, Any], authenticated: bool = True) -> dict[str, Any]:
|
||||||
async with httpx.AsyncClient(timeout=30) as client:
|
async with httpx.AsyncClient(timeout=30) as client:
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
|
|||||||
Reference in New Issue
Block a user