ux: LBC Styling, feat: thinking, feat: more tools:

This commit is contained in:
2026-05-06 01:15:37 -04:00
parent 36c91ce500
commit 5850674448
9 changed files with 1160 additions and 165 deletions
+427 -2
View File
@@ -12,12 +12,109 @@ from traderai.uex_client import UEXClient
ToolHandler = Callable[..., Awaitable[dict[str, Any]]]
UEX_GET_RESOURCES: dict[str, dict[str, Any]] = {
"categories": {"params": ["type", "section"], "auth": False, "group": "reference"},
"categories_attributes": {"params": ["id_category", "category_name", "category_type"], "auth": False, "group": "reference"},
"cities": {"params": ["id", "id_planet", "id_star_system", "name", "slug"], "auth": False, "group": "locations"},
"commodities": {"params": ["id", "name", "code", "slug"], "auth": False, "group": "trade"},
"commodities_alerts": {"params": ["id_commodity", "commodity_name", "commodity_code", "commodity_slug"], "auth": False, "group": "trade"},
"commodities_averages": {"params": ["id_commodity", "commodity_name", "commodity_code", "commodity_slug"], "auth": False, "group": "trade"},
"commodities_prices": {
"params": ["id_terminal", "id_commodity", "terminal_name", "terminal_code", "terminal_slug", "commodity_name", "commodity_code", "commodity_slug"],
"auth": False,
"group": "trade",
},
"commodities_prices_all": {"params": [], "auth": False, "group": "trade", "heavy": True},
"commodities_prices_history": {"params": ["id_commodity", "id_terminal", "commodity_name", "terminal_name"], "auth": False, "group": "trade"},
"commodities_ranking": {"params": ["id_commodity", "commodity_name", "commodity_code", "commodity_slug"], "auth": False, "group": "trade"},
"commodities_raw_averages": {"params": ["id_commodity", "commodity_name", "commodity_code", "commodity_slug"], "auth": False, "group": "mining"},
"commodities_raw_prices": {"params": ["id_terminal", "id_commodity", "terminal_name", "commodity_name"], "auth": False, "group": "mining"},
"commodities_raw_prices_all": {"params": [], "auth": False, "group": "mining", "heavy": True},
"commodities_routes": {"params": ["id_terminal_origin", "id_terminal_destination", "id_commodity", "terminal_origin_name", "terminal_destination_name", "commodity_name"], "auth": False, "group": "trade"},
"commodities_status": {"params": [], "auth": False, "group": "trade"},
"companies": {"params": ["id", "name", "code"], "auth": False, "group": "reference"},
"contacts": {"params": ["id", "name"], "auth": False, "group": "reference"},
"contracts": {"params": ["id", "name", "slug"], "auth": False, "group": "reference"},
"crew": {"params": ["id", "name", "slug"], "auth": False, "group": "reference"},
"currencies_index": {"params": ["code"], "auth": False, "group": "reference"},
"currencies_index_history": {"params": ["code"], "auth": False, "group": "reference"},
"data_extract": {"params": ["table"], "auth": False, "group": "data"},
"data_parameters": {"params": ["endpoint"], "auth": False, "group": "data"},
"factions": {"params": ["id", "name", "slug"], "auth": False, "group": "reference"},
"fleet": {"params": ["username"], "auth": False, "group": "vehicles"},
"fuel_prices": {"params": ["id_terminal", "terminal_name", "terminal_code", "terminal_slug"], "auth": False, "group": "trade"},
"fuel_prices_all": {"params": [], "auth": False, "group": "trade", "heavy": True},
"game_versions": {"params": [], "auth": False, "group": "reference"},
"items": {"params": ["id", "id_category", "name", "uuid", "slug"], "auth": False, "group": "items"},
"items_attributes": {"params": ["id_item", "item_name", "item_slug"], "auth": False, "group": "items"},
"items_prices": {"params": ["id_item", "id_terminal", "item_name", "terminal_name"], "auth": False, "group": "items"},
"items_prices_all": {"params": [], "auth": False, "group": "items", "heavy": True},
"jump_points": {"params": ["id", "name", "slug"], "auth": False, "group": "locations"},
"jurisdictions": {"params": ["id", "name"], "auth": False, "group": "locations"},
"marketplace_averages": {"params": ["id_item", "item_name", "item_slug"], "auth": False, "group": "marketplace"},
"marketplace_averages_all": {"params": [], "auth": False, "group": "marketplace", "heavy": True},
"marketplace_favorites": {"params": ["id_listing"], "auth": True, "group": "marketplace"},
"marketplace_listings": {"params": ["id", "slug", "username"], "auth": False, "group": "marketplace"},
"marketplace_negotiations": {"params": ["id", "id_listing", "hash"], "auth": True, "group": "marketplace"},
"marketplace_negotiations_messages": {"params": ["hash", "id_negotiation"], "auth": True, "group": "marketplace"},
"marketplace_prices_averages": {"params": ["id_item", "item_name", "item_slug"], "auth": False, "group": "marketplace"},
"marketplace_prices_averages_all": {"params": [], "auth": False, "group": "marketplace", "heavy": True},
"marketplace_prices_history": {"params": ["id_item", "id_listing", "item_name"], "auth": False, "group": "marketplace"},
"marketplace_trends": {"params": ["id_item", "item_name", "item_slug"], "auth": False, "group": "marketplace"},
"moons": {"params": ["id", "id_planet", "id_star_system", "name", "slug"], "auth": False, "group": "locations"},
"orbits": {"params": ["id", "id_star_system", "name", "slug"], "auth": False, "group": "locations"},
"orbits_distances": {"params": ["id_origin", "id_destination"], "auth": False, "group": "locations"},
"organizations": {"params": ["sid", "name"], "auth": False, "group": "reference"},
"outposts": {"params": ["id", "id_moon", "id_planet", "name", "slug"], "auth": False, "group": "locations"},
"planets": {"params": ["id", "id_star_system", "name", "slug"], "auth": False, "group": "locations"},
"poi": {"params": ["id", "id_star_system", "name", "slug"], "auth": False, "group": "locations"},
"refineries_audits": {"params": ["id_terminal", "terminal_name"], "auth": False, "group": "mining"},
"refineries_capacities": {"params": ["id_terminal", "terminal_name"], "auth": False, "group": "mining"},
"refineries_methods": {"params": ["id", "name"], "auth": False, "group": "mining"},
"refineries_yields": {"params": ["id_terminal", "id_commodity", "terminal_name", "commodity_name"], "auth": False, "group": "mining"},
"release_notes": {"params": [], "auth": False, "group": "reference"},
"space_stations": {"params": ["id", "id_star_system", "id_planet", "id_moon", "name", "slug"], "auth": False, "group": "locations"},
"star_systems": {"params": ["id", "name", "code", "slug"], "auth": False, "group": "locations"},
"terminals": {"params": ["id", "id_star_system", "name", "code", "slug"], "auth": False, "group": "locations"},
"terminals_distances": {"params": ["id_terminal_origin", "id_terminal_destination"], "auth": False, "group": "locations"},
"user": {"params": ["username"], "auth": False, "group": "user"},
"user_notifications": {"params": [], "auth": True, "group": "user"},
"user_refineries_jobs": {"params": ["id"], "auth": True, "group": "user"},
"user_trades": {"params": ["id"], "auth": True, "group": "user"},
"vehicles": {"params": ["id", "name", "slug", "uuid"], "auth": False, "group": "vehicles"},
"vehicles_loaners": {"params": ["id_vehicle", "vehicle_name", "vehicle_slug"], "auth": False, "group": "vehicles"},
"vehicles_prices": {"params": ["id_vehicle", "vehicle_name", "vehicle_slug"], "auth": False, "group": "vehicles"},
"vehicles_purchases_prices": {"params": ["id_vehicle", "id_terminal", "vehicle_name", "terminal_name"], "auth": False, "group": "vehicles"},
"vehicles_purchases_prices_all": {"params": [], "auth": False, "group": "vehicles", "heavy": True},
"vehicles_rentals_prices": {"params": ["id_vehicle", "id_terminal", "vehicle_name", "terminal_name"], "auth": False, "group": "vehicles"},
"vehicles_rentals_prices_all": {"params": [], "auth": False, "group": "vehicles", "heavy": True},
"wallet_balance": {"params": [], "auth": True, "group": "user"},
}
UEX_POST_RESOURCES = {
"data_submit",
"marketplace_advertise",
"marketplace_negotiations_messages",
"user_refineries_jobs_add",
"user_trades_add",
"user_trades_edit",
"wallet_add",
}
UEX_DELETE_RESOURCES = {
"marketplace_listings",
"user_refineries_jobs_remove",
"user_trades_remove",
}
@dataclass
class PendingAction:
id: str
label: str
endpoint: str
payload: dict[str, Any]
method: str = "POST"
class ToolRegistry:
@@ -46,10 +143,23 @@ class ToolRegistry:
"list_wake_jobs": self.list_wake_jobs,
"check_uex_notifications": self.check_uex_notifications,
}
self.handlers["uex_api_catalog"] = self.uex_api_catalog
self.handlers["uex_get"] = self.uex_get
self.handlers["uex_draft_post"] = self.uex_draft_post
self.handlers["uex_draft_delete"] = self.uex_draft_delete
for resource in UEX_GET_RESOURCES:
self.handlers[self._get_tool_name(resource)] = self._make_get_handler(resource)
for resource in UEX_POST_RESOURCES:
self.handlers[self._post_tool_name(resource)] = self._make_post_handler(resource)
for resource in UEX_DELETE_RESOURCES:
self.handlers[self._delete_tool_name(resource)] = self._make_delete_handler(resource)
@property
def schemas(self) -> list[dict[str, Any]]:
return [
*self._uex_get_schemas(),
*self._uex_post_schemas(),
*self._uex_delete_schemas(),
{
"type": "function",
"function": {
@@ -239,6 +349,8 @@ class ToolRegistry:
action = self.pending_actions.pop(action_id, None)
if not action:
return {"error": f"Pending action not found: {action_id}"}
if action.method == "DELETE":
return await self.uex.delete(action.endpoint, action.payload, authenticated=True)
return await self.uex.post(action.endpoint, action.payload, authenticated=True)
async def decline(self, action_id: str) -> dict[str, Any]:
@@ -250,11 +362,232 @@ class ToolRegistry:
"pending_action": {
"id": action.id,
"label": action.label,
"method": action.method,
"endpoint": action.endpoint,
"payload": action.payload,
},
}
async def uex_api_catalog(self, group: str | None = None, resource: str | None = None) -> dict[str, Any]:
if resource:
key = self._validate_resource(resource, UEX_GET_RESOURCES)
info = UEX_GET_RESOURCES[key]
return {
"resource": key,
"method": "GET",
"group": info["group"],
"authenticated": info["auth"],
"heavy": bool(info.get("heavy")),
"params": info["params"],
"write_resources": {
"post": sorted(UEX_POST_RESOURCES),
"delete": sorted(UEX_DELETE_RESOURCES),
},
}
grouped: dict[str, list[dict[str, Any]]] = {}
for name, info in sorted(UEX_GET_RESOURCES.items()):
if group and info["group"] != group:
continue
grouped.setdefault(info["group"], []).append(
{
"resource": name,
"params": info["params"],
"auth": info["auth"],
"heavy": bool(info.get("heavy")),
}
)
return {
"get": grouped,
"post": sorted(UEX_POST_RESOURCES),
"delete": sorted(UEX_DELETE_RESOURCES),
"usage": "Call uex_get(resource, params, fields, limit, mode). Use fields and limit to keep responses small.",
}
async def uex_get(
self,
resource: str,
params: dict[str, Any] | None = None,
fields: list[str] | None = None,
search: str | None = None,
limit: int = 10,
offset: int = 0,
mode: str = "summary",
) -> dict[str, Any]:
resource = self._validate_resource(resource, UEX_GET_RESOURCES)
info = UEX_GET_RESOURCES[resource]
cleaned_params = self._filter_params(params or {}, info["params"])
response = await self.uex.get(resource, cleaned_params, authenticated=bool(info["auth"]))
data = response.get("data")
items = self._as_list(data)
total = len(items)
if search:
needle = search.casefold()
items = [item for item in items if needle in self._search_text(item)]
filtered_total = len(items)
offset = max(0, offset)
limit = max(1, min(limit, 100))
window = items[offset : offset + limit]
compacted = [
self._project_item(item, fields=fields, mode=mode)
for item in window
]
return {
"status": response.get("status"),
"resource": resource,
"params": cleaned_params,
"total": total,
"matched": filtered_total,
"returned": len(compacted),
"offset": offset,
"truncated": offset + len(compacted) < filtered_total,
"items": compacted,
}
async def uex_draft_post(self, resource: str, payload: dict[str, Any], label: str | None = None) -> dict[str, Any]:
resource = self._validate_resource(resource, UEX_POST_RESOURCES)
return self._pending(label or f"POST {resource}", resource, payload, method="POST")
async def uex_draft_delete(
self,
resource: str,
params: dict[str, Any] | None = None,
label: str | None = None,
) -> dict[str, Any]:
resource = self._validate_resource(resource, UEX_DELETE_RESOURCES)
return self._pending(label or f"DELETE {resource}", resource, params or {}, method="DELETE")
def _make_get_handler(self, resource: str) -> ToolHandler:
async def handler(**arguments: Any) -> dict[str, Any]:
fields = arguments.pop("fields", None)
search = arguments.pop("search", None)
limit = arguments.pop("limit", 10)
offset = arguments.pop("offset", 0)
mode = arguments.pop("mode", "summary")
return await self.uex_get(
resource,
params=arguments,
fields=fields,
search=search,
limit=limit,
offset=offset,
mode=mode,
)
return handler
def _make_post_handler(self, resource: str) -> ToolHandler:
async def handler(payload: dict[str, Any], label: str | None = None) -> dict[str, Any]:
return await self.uex_draft_post(resource, payload, label=label)
return handler
def _make_delete_handler(self, resource: str) -> ToolHandler:
async def handler(label: str | None = None, **params: Any) -> dict[str, Any]:
return await self.uex_draft_delete(resource, params, label=label)
return handler
@classmethod
def _uex_get_schemas(cls) -> list[dict[str, Any]]:
return [
{
"type": "function",
"function": {
"name": cls._get_tool_name(resource),
"description": cls._get_tool_description(resource, info),
"parameters": cls._get_tool_parameters(info["params"]),
},
}
for resource, info in sorted(UEX_GET_RESOURCES.items())
]
@classmethod
def _uex_post_schemas(cls) -> list[dict[str, Any]]:
return [
{
"type": "function",
"function": {
"name": cls._post_tool_name(resource),
"description": f"Draft UEX POST /{resource}/ for user approval. Nothing is sent until approval.",
"parameters": {
"type": "object",
"required": ["payload"],
"properties": {
"payload": {"type": "object", "description": f"JSON body for UEX POST /{resource}/."},
"label": {"type": "string", "description": "Short approval label."},
},
},
},
}
for resource in sorted(UEX_POST_RESOURCES)
]
@classmethod
def _uex_delete_schemas(cls) -> list[dict[str, Any]]:
return [
{
"type": "function",
"function": {
"name": cls._delete_tool_name(resource),
"description": f"Draft UEX DELETE /{resource}/ for user approval. Nothing is deleted until approval.",
"parameters": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"label": {"type": "string", "description": "Short approval label."},
},
},
},
}
for resource in sorted(UEX_DELETE_RESOURCES)
]
@classmethod
def _get_tool_parameters(cls, endpoint_params: list[str]) -> dict[str, Any]:
properties = {
param: cls._query_param_schema(param)
for param in endpoint_params
}
properties.update(
{
"fields": {
"type": "array",
"items": {"type": "string"},
"description": "Fields to keep in each result row.",
},
"search": {"type": "string", "description": "Local text filter after UEX returns data."},
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 10},
"offset": {"type": "integer", "minimum": 0, "default": 0},
"mode": {"type": "string", "enum": ["summary", "full"], "default": "summary"},
}
)
return {"type": "object", "properties": properties}
@staticmethod
def _query_param_schema(param: str) -> dict[str, Any]:
if param == "id" or param.startswith("id_"):
return {"type": "integer"}
return {"type": "string"}
@staticmethod
def _get_tool_description(resource: str, info: dict[str, Any]) -> str:
auth = " Authenticated." if info["auth"] else ""
heavy = " Heavy endpoint; use fields and limit." if info.get("heavy") else ""
return f"GET UEX /{resource}/ with compact, token-limited results.{auth}{heavy}"
@staticmethod
def _get_tool_name(resource: str) -> str:
return f"get_uex_{resource}"
@staticmethod
def _post_tool_name(resource: str) -> str:
return f"draft_uex_{resource}"
@staticmethod
def _delete_tool_name(resource: str) -> str:
return f"delete_uex_{resource}"
async def search_marketplace_listings(
self,
query: str | None = None,
@@ -353,20 +686,112 @@ class ToolRegistry:
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], method: str = "POST") -> 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)
self.pending_actions[action_id] = PendingAction(action_id, label, endpoint, payload, method)
return {
"pending_action": {
"id": action_id,
"label": label,
"method": method,
"endpoint": endpoint,
"payload": payload,
"approval_required": self.require_write_approval,
}
}
@staticmethod
def _validate_resource(resource: str, allowed: dict[str, Any] | set[str]) -> str:
normalized = resource.strip().strip("/").casefold()
if normalized not in allowed:
choices = sorted(allowed.keys() if isinstance(allowed, dict) else allowed)
near = [name for name in choices if normalized in name or name in normalized][:8]
hint = f" Did you mean: {', '.join(near)}?" if near else ""
raise ValueError(f"Unsupported UEX resource: {resource}.{hint}")
return normalized
@staticmethod
def _filter_params(params: dict[str, Any], allowed_params: list[str]) -> dict[str, Any]:
if not allowed_params:
return {key: value for key, value in params.items() if value is not None}
allowed = set(allowed_params)
return {key: value for key, value in params.items() if key in allowed and value is not None}
@staticmethod
def _as_list(data: Any) -> list[Any]:
if data is None:
return []
if isinstance(data, list):
return data
return [data]
@classmethod
def _project_item(cls, item: Any, fields: list[str] | None = None, mode: str = "summary") -> Any:
if not isinstance(item, dict):
return item
if fields:
return {field: cls._compact_scalar(item.get(field)) for field in fields if field in item}
if mode == "full":
return {key: cls._compact_scalar(value) for key, value in item.items()}
priority = [
"id",
"uuid",
"code",
"slug",
"name",
"title",
"type",
"section",
"operation",
"price",
"currency",
"unit",
"location",
"terminal_name",
"commodity_name",
"item_name",
"vehicle_name",
"price_buy",
"price_sell",
"scu_buy",
"scu_sell",
"scu_sell_stock",
"status_buy",
"status_sell",
"date_modified",
"date_added",
]
selected: dict[str, Any] = {}
for key in priority:
if key in item and item[key] not in (None, ""):
selected[key] = cls._compact_scalar(item[key])
for key, value in item.items():
if len(selected) >= 16:
break
if key in selected or value in (None, ""):
continue
if isinstance(value, (str, int, float, bool)):
selected[key] = cls._compact_scalar(value)
return selected
@staticmethod
def _compact_scalar(value: Any) -> Any:
if isinstance(value, str) and len(value) > 240:
return value[:237] + "..."
if isinstance(value, list):
return value[:5]
if isinstance(value, dict):
return {key: nested_value for key, nested_value in list(value.items())[:12]}
return value
@classmethod
def _search_text(cls, item: Any) -> str:
if isinstance(item, dict):
return " ".join(str(value) for value in item.values() if isinstance(value, (str, int, float))).casefold()
return str(item).casefold()
@staticmethod
def _summarize_listing(listing: dict[str, Any]) -> dict[str, Any]:
return {