Inital Commit

This commit is contained in:
2026-05-05 19:45:12 -04:00
parent 729f421ec8
commit dbc97bddee
21 changed files with 3238 additions and 2 deletions
+357
View File
@@ -0,0 +1,357 @@
from __future__ import annotations
import uuid
from dataclasses import dataclass
from typing import Any, Awaitable, Callable
from traderai.memory import MemoryStore
from traderai.scheduler import WakeScheduler
from traderai.uex_client import UEXClient
ToolHandler = Callable[..., Awaitable[dict[str, Any]]]
@dataclass
class PendingAction:
id: str
label: str
endpoint: str
payload: dict[str, Any]
class ToolRegistry:
def __init__(
self,
uex: UEXClient,
require_write_approval: bool = True,
memory: MemoryStore | None = None,
scheduler: WakeScheduler | None = None,
) -> None:
self.uex = uex
self.require_write_approval = require_write_approval
self.memory = memory
self.scheduler = scheduler
self.pending_actions: dict[str, PendingAction] = {}
self.handlers: dict[str, ToolHandler] = {
"search_marketplace_listings": self.search_marketplace_listings,
"get_marketplace_listing": self.get_marketplace_listing,
"list_marketplace_negotiations": self.list_marketplace_negotiations,
"get_negotiation_messages": self.get_negotiation_messages,
"draft_negotiation_message": self.draft_negotiation_message,
"draft_marketplace_listing": self.draft_marketplace_listing,
"remember_user_fact": self.remember_user_fact,
"recall_memory": self.recall_memory,
"schedule_wake_job": self.schedule_wake_job,
"list_wake_jobs": self.list_wake_jobs,
}
@property
def schemas(self) -> list[dict[str, Any]]:
return [
{
"type": "function",
"function": {
"name": "search_marketplace_listings",
"description": "Search active UEX marketplace listings. UEX returns up to 100 active listings; filters are applied locally.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Text to search in title, description, location, advertiser, or slug."},
"operation": {"type": "string", "enum": ["buy", "sell"]},
"type": {"type": "string", "enum": ["item", "service", "contract"]},
"username": {"type": "string", "description": "Advertiser IGN."},
"location": {"type": "string"},
"min_price": {"type": "number"},
"max_price": {"type": "number"},
"limit": {"type": "integer", "minimum": 1, "maximum": 25},
},
},
},
},
{
"type": "function",
"function": {
"name": "get_marketplace_listing",
"description": "Fetch a specific UEX marketplace listing by id or slug.",
"parameters": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"slug": {"type": "string"},
},
},
},
},
{
"type": "function",
"function": {
"name": "list_marketplace_negotiations",
"description": "List authenticated marketplace negotiations for the configured UEX user.",
"parameters": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"id_listing": {"type": "integer"},
"hash": {"type": "string"},
},
},
},
},
{
"type": "function",
"function": {
"name": "get_negotiation_messages",
"description": "Fetch authenticated messages from a marketplace negotiation by hash or id_negotiation.",
"parameters": {
"type": "object",
"properties": {
"hash": {"type": "string"},
"id_negotiation": {"type": "integer"},
},
},
},
},
{
"type": "function",
"function": {
"name": "draft_negotiation_message",
"description": "Draft a message or offer to a UEX negotiation. This creates a pending action that must be approved before sending.",
"parameters": {
"type": "object",
"required": ["message"],
"properties": {
"message": {"type": "string"},
"hash": {"type": "string"},
"id_negotiation": {"type": "integer"},
"is_production": {"type": "integer", "enum": [0, 1], "default": 0},
},
},
},
},
{
"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.",
"parameters": {
"type": "object",
"required": ["id_category", "operation", "type", "unit", "title", "description", "price", "currency", "language"],
"properties": {
"id_item": {"type": "integer"},
"id_star_system": {"type": "integer"},
"id_organization": {"type": "integer"},
"id_category": {"type": "integer"},
"operation": {"type": "string", "enum": ["buy", "sell"]},
"type": {"type": "string", "enum": ["item", "service", "contract"]},
"unit": {"type": "string"},
"title": {"type": "string"},
"description": {"type": "string"},
"price": {"type": "number"},
"currency": {"type": "string", "enum": ["UEC", "WIF"]},
"language": {"type": "string", "default": "en_US"},
"location": {"type": "string"},
"source": {"type": "string"},
"availability": {"type": "string"},
"in_stock": {"type": "integer"},
"hours_expiration": {"type": "integer"},
"is_hidden": {"type": "integer", "enum": [0, 1]},
"is_production": {"type": "integer", "enum": [0, 1], "default": 0},
},
},
},
},
{
"type": "function",
"function": {
"name": "remember_user_fact",
"description": "Persist a durable user preference, identity detail, trading rule, or long-term note for future chats.",
"parameters": {
"type": "object",
"required": ["content"],
"properties": {
"content": {"type": "string"},
"kind": {"type": "string", "enum": ["user", "preference", "trading", "project", "note"], "default": "note"},
"importance": {"type": "integer", "minimum": 1, "maximum": 5, "default": 3},
},
},
},
},
{
"type": "function",
"function": {
"name": "recall_memory",
"description": "Search long-term memory for relevant prior facts, preferences, and chat context.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"},
"limit": {"type": "integer", "minimum": 1, "maximum": 10, "default": 6},
},
},
},
},
{
"type": "function",
"function": {
"name": "schedule_wake_job",
"description": "Create a scheduled wake-up job for the assistant. Use either run_at for one-time jobs or cron for recurring jobs.",
"parameters": {
"type": "object",
"required": ["prompt"],
"properties": {
"prompt": {"type": "string", "description": "What the AI should consider or do when it wakes."},
"run_at": {"type": "string", "description": "ISO datetime for a one-time wake job, such as 2026-05-05T20:30:00-04:00."},
"cron": {"type": "string", "description": "Five-field cron expression for recurring jobs, such as 0 9 * * *."},
},
},
},
},
{
"type": "function",
"function": {
"name": "list_wake_jobs",
"description": "List currently enabled scheduled assistant wake jobs.",
"parameters": {"type": "object", "properties": {}},
},
},
]
async def execute(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
handler = self.handlers.get(name)
if not handler:
return {"error": f"Unknown tool: {name}"}
try:
return await handler(**arguments)
except Exception as exc:
return {"error": str(exc)}
async def approve(self, action_id: str) -> dict[str, Any]:
action = self.pending_actions.pop(action_id, None)
if not action:
return {"error": f"Pending action not found: {action_id}"}
return await self.uex.post(action.endpoint, action.payload, authenticated=True)
async def search_marketplace_listings(
self,
query: str | None = None,
operation: str | None = None,
type: str | None = None,
username: str | None = None,
location: str | None = None,
min_price: float | None = None,
max_price: float | None = None,
limit: int = 10,
) -> dict[str, Any]:
response = await self.uex.get("marketplace_listings", {"username": username})
listings = response.get("data") or []
filtered = []
q = (query or "").casefold()
loc = (location or "").casefold()
for listing in listings:
if operation and listing.get("operation") != operation:
continue
if type and listing.get("type") != type:
continue
if min_price is not None and float(listing.get("price") or 0) < min_price:
continue
if max_price is not None and float(listing.get("price") or 0) > max_price:
continue
if loc and loc not in str(listing.get("location") or "").casefold():
continue
haystack = " ".join(str(listing.get(k) or "") for k in ["title", "description", "location", "user_username", "slug"]).casefold()
if q and q not in haystack:
continue
filtered.append(self._summarize_listing(listing))
if len(filtered) >= max(1, min(limit, 25)):
break
return {"count": len(filtered), "listings": filtered}
async def get_marketplace_listing(self, id: int | None = None, slug: str | None = None) -> dict[str, Any]:
response = await self.uex.get("marketplace_listings", {"id": id, "slug": slug})
return {"listing": response.get("data")}
async def list_marketplace_negotiations(
self,
id: int | None = None,
id_listing: int | None = None,
hash: str | None = None,
) -> dict[str, Any]:
return await self.uex.get("marketplace_negotiations", {"id": id, "id_listing": id_listing, "hash": hash}, authenticated=True)
async def get_negotiation_messages(self, hash: str | None = None, id_negotiation: int | None = None) -> dict[str, Any]:
return await self.uex.get("marketplace_negotiations_messages", {"hash": hash, "id_negotiation": id_negotiation}, authenticated=True)
async def draft_negotiation_message(
self,
message: str,
hash: str | None = None,
id_negotiation: int | None = None,
is_production: int = 0,
) -> dict[str, Any]:
payload = {"message": message, "hash": hash, "id_negotiation": id_negotiation, "is_production": is_production}
return self._pending("Send negotiation message", "marketplace_negotiations_messages", payload)
async def draft_marketplace_listing(self, **payload: Any) -> dict[str, Any]:
return self._pending("Post marketplace listing", "marketplace_advertise", payload)
async def remember_user_fact(self, content: str, kind: str = "note", importance: int = 3) -> dict[str, Any]:
if self.memory is None:
return {"error": "Memory store is not configured."}
return {"memory": self.memory.remember(kind, content, importance)}
async def recall_memory(self, query: str = "", limit: int = 6) -> dict[str, Any]:
if self.memory is None:
return {"error": "Memory store is not configured."}
return {"memories": self.memory.recall(query, max(1, min(limit, 10)))}
async def schedule_wake_job(
self,
prompt: str,
run_at: str | None = None,
cron: str | None = None,
) -> dict[str, Any]:
if self.scheduler is None:
return {"error": "Scheduler is not configured."}
if bool(run_at) == bool(cron):
return {"error": "Provide exactly one of run_at or cron."}
if run_at:
return {"scheduled_job": self.scheduler.schedule_date(run_at, prompt)}
return {"scheduled_job": self.scheduler.schedule_cron(cron or "", prompt)}
async def list_wake_jobs(self) -> dict[str, Any]:
if self.scheduler is None:
return {"error": "Scheduler is not configured."}
return {"scheduled_jobs": self.scheduler.list_jobs()}
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}
self.pending_actions[action_id] = PendingAction(action_id, label, endpoint, payload)
return {
"pending_action": {
"id": action_id,
"label": label,
"endpoint": endpoint,
"payload": payload,
"approval_required": self.require_write_approval,
}
}
@staticmethod
def _summarize_listing(listing: dict[str, Any]) -> dict[str, Any]:
return {
"id": listing.get("id"),
"slug": listing.get("slug"),
"title": listing.get("title"),
"operation": listing.get("operation"),
"type": listing.get("type"),
"price": listing.get("price"),
"currency": listing.get("currency"),
"unit": listing.get("unit"),
"location": listing.get("location"),
"availability": listing.get("availability"),
"in_stock": listing.get("in_stock"),
"advertiser": listing.get("user_username"),
"expires_at": listing.get("date_expiration"),
}