agent: look at current info, its aUEC, feat: pull up notifcations.

This commit is contained in:
2026-05-05 20:05:33 -04:00
parent f7ac45ddd8
commit 761eda6155
11 changed files with 198 additions and 8 deletions
+13 -1
View File
@@ -12,7 +12,9 @@ from traderai.tools import ToolRegistry
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.
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,
memory: MemoryStore | None = None,
user_name: str | None = None,
num_ctx: int | None = None,
) -> None:
self.base_url = base_url.rstrip("/")
self.model = model
self.tools = tools
self.memory = memory
self.user_name = user_name
self.num_ctx = num_ctx
self.messages: list[dict[str, Any]] = [{"role": "system", "content": SYSTEM_PROMPT}]
async def health(self) -> dict[str, Any]:
@@ -165,6 +169,7 @@ class OllamaAgent:
"model": self.model,
"messages": self._messages_with_context(query, previous_interaction=previous_interaction),
"tools": self.tools.schemas,
"options": self._ollama_options(),
"stream": False,
},
)
@@ -184,6 +189,7 @@ class OllamaAgent:
"model": self.model,
"messages": self._messages_with_context(query, previous_interaction=previous_interaction),
"tools": self.tools.schemas,
"options": self._ollama_options(),
"stream": True,
},
) as response:
@@ -258,6 +264,11 @@ class OllamaAgent:
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
def _tool_status(name: str) -> str:
labels = {
@@ -267,6 +278,7 @@ class OllamaAgent:
"get_negotiation_messages": "Reading negotiation messages",
"draft_negotiation_message": "Drafting message for approval",
"draft_marketplace_listing": "Drafting listing for approval",
"check_uex_notifications": "Checking UEX notifications",
}
return labels.get(name, f"Running {name}")
+2
View File
@@ -9,11 +9,13 @@ class Settings(BaseSettings):
ollama_base_url: str = "http://localhost:11434"
ollama_model: str = "qwen3.5:9b"
ollama_num_ctx: int = 64000
uex_base_url: str = "https://api.uexcorp.space/2.0"
uex_secret_key: str | None = Field(default=None)
uex_bearer_token: str | None = Field(default=None)
traderai_user_name: str | None = Field(default=None)
traderai_memory_path: str = "data/traderai.sqlite3"
uex_notification_poll_seconds: int = 60
require_write_approval: bool = True
+67
View File
@@ -7,23 +7,34 @@ from uuid import uuid4
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
from apscheduler.triggers.interval import IntervalTrigger
from tzlocal import get_localzone
from traderai.memory import MemoryStore, iso_now, time_since
UEX_NOTIFICATION_JOB_ID = "uex-notification-poll"
class WakeScheduler:
def __init__(self, memory: MemoryStore) -> None:
self.memory = memory
self.scheduler = AsyncIOScheduler(timezone=get_localzone())
self.agent = None
self.uex = None
self.notification_poll_seconds = 60
def bind_agent(self, agent: Any) -> None:
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:
if not self.scheduler.running:
self.scheduler.start()
self._schedule_notification_poll()
for job in self.memory.list_jobs():
self._schedule_existing(job)
@@ -77,3 +88,59 @@ class WakeScheduler:
text = await self.agent.generate_wake_response(wake_message)
self.memory.add_outbox(text)
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
View File
@@ -35,8 +35,16 @@ def create_app() -> FastAPI:
scheduler = WakeScheduler(memory)
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)
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_uex_notifications(uex, settings.uex_notification_poll_seconds)
app = FastAPI(title="TraderAI")
static_dir = Path(__file__).resolve().parent.parent / "web"
+17 -2
View File
@@ -44,6 +44,7 @@ class ToolRegistry:
"recall_memory": self.recall_memory,
"schedule_wake_job": self.schedule_wake_job,
"list_wake_jobs": self.list_wake_jobs,
"check_uex_notifications": self.check_uex_notifications,
}
@property
@@ -53,7 +54,7 @@ class ToolRegistry:
"type": "function",
"function": {
"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": {
"type": "object",
"properties": {
@@ -133,7 +134,7 @@ class ToolRegistry:
"type": "function",
"function": {
"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": {
"type": "object",
"required": ["id_category", "operation", "type", "unit", "title", "description", "price", "currency", "language"],
@@ -215,6 +216,14 @@ class ToolRegistry:
"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]:
@@ -324,6 +333,12 @@ class ToolRegistry:
return {"error": "Scheduler is not configured."}
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]:
action_id = str(uuid.uuid4())
payload = {key: value for key, value in payload.items() if value is not None}
+7
View File
@@ -42,6 +42,13 @@ class UEXClient:
data = data[0] if data else None
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 with httpx.AsyncClient(timeout=30) as client:
response = await client.post(