feat: history tools
This commit is contained in:
@@ -14,6 +14,7 @@ from traderai.tools import ToolRegistry
|
||||
SYSTEM_PROMPT = """You are TraderAI, a local assistant for UEX marketplace work.
|
||||
Use tools when the user asks about UEX data, open/current listings, active negotiations, unread notifications, messages, offers, or posting ads.
|
||||
Use the specific UEX tool for the needed endpoint, such as get_uex_commodities_prices or get_uex_vehicles. Use fields, limit, and summary mode so tool results stay compact.
|
||||
When the user asks for history, trends, changes over time, or past prices, prefer the summarize_uex_*_history tools when available; use search_uex_api_index(history_only=true) if you need to discover history endpoints.
|
||||
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.
|
||||
@@ -280,6 +281,10 @@ class OllamaAgent:
|
||||
if name.startswith("delete_uex_"):
|
||||
return f"Drafting UEX {name.removeprefix('delete_uex_')} delete for approval"
|
||||
labels = {
|
||||
"search_uex_api_index": "Searching UEX API index",
|
||||
"summarize_uex_commodity_price_history": "Summarizing commodity price history",
|
||||
"summarize_uex_marketplace_price_history": "Summarizing marketplace price history",
|
||||
"summarize_uex_currency_index_history": "Summarizing currency index history",
|
||||
"uex_api_catalog": "Checking UEX API catalog",
|
||||
"uex_get": "Fetching UEX data",
|
||||
"uex_draft_post": "Drafting UEX write for approval",
|
||||
|
||||
+363
-5
@@ -25,7 +25,7 @@ UEX_GET_RESOURCES: dict[str, dict[str, Any]] = {
|
||||
"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_prices_history": {"params": ["id_terminal", "id_commodity", "game_version"], "auth": False, "group": "trade", "history": True},
|
||||
"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"},
|
||||
@@ -37,7 +37,7 @@ UEX_GET_RESOURCES: dict[str, dict[str, Any]] = {
|
||||
"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"},
|
||||
"currencies_index_history": {"params": ["currency", "date_from", "date_to"], "auth": False, "group": "reference", "history": True},
|
||||
"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"},
|
||||
@@ -59,7 +59,26 @@ UEX_GET_RESOURCES: dict[str, dict[str, Any]] = {
|
||||
"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_prices_history": {
|
||||
"params": [
|
||||
"id_item",
|
||||
"id_listing",
|
||||
"id_terminal",
|
||||
"id_star_system",
|
||||
"id_category",
|
||||
"item_uuid",
|
||||
"item_name",
|
||||
"operation",
|
||||
"quality_tier",
|
||||
"currency",
|
||||
"game_version",
|
||||
"date_start",
|
||||
"date_end",
|
||||
],
|
||||
"auth": False,
|
||||
"group": "marketplace",
|
||||
"history": True,
|
||||
},
|
||||
"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"},
|
||||
@@ -107,6 +126,12 @@ UEX_DELETE_RESOURCES = {
|
||||
"user_trades_remove",
|
||||
}
|
||||
|
||||
UEX_RESOURCE_DESCRIPTIONS = {
|
||||
"commodities_prices_history": "Historical commodity prices at a terminal. Requires id_terminal and id_commodity; accepts game_version. UEX limits this to 500 rows.",
|
||||
"marketplace_prices_history": "Historical marketplace price snapshots, one row per listing per price change. Requires at least one filter; supports date_start/date_end and up to 1000 records.",
|
||||
"currencies_index_history": "Historical UEX currency index snapshots with basket component detail. Supports currency, date_from, and date_to timestamps.",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class PendingAction:
|
||||
@@ -147,6 +172,10 @@ class ToolRegistry:
|
||||
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
|
||||
self.handlers["search_uex_api_index"] = self.search_uex_api_index
|
||||
self.handlers["summarize_uex_commodity_price_history"] = self.summarize_uex_commodity_price_history
|
||||
self.handlers["summarize_uex_marketplace_price_history"] = self.summarize_uex_marketplace_price_history
|
||||
self.handlers["summarize_uex_currency_index_history"] = self.summarize_uex_currency_index_history
|
||||
for resource in UEX_GET_RESOURCES:
|
||||
self.handlers[self._get_tool_name(resource)] = self._make_get_handler(resource)
|
||||
for resource in UEX_POST_RESOURCES:
|
||||
@@ -157,7 +186,9 @@ class ToolRegistry:
|
||||
@property
|
||||
def schemas(self) -> list[dict[str, Any]]:
|
||||
return [
|
||||
self._api_index_schema(),
|
||||
*self._uex_get_schemas(),
|
||||
*self._history_summary_schemas(),
|
||||
*self._uex_post_schemas(),
|
||||
*self._uex_delete_schemas(),
|
||||
{
|
||||
@@ -457,6 +488,145 @@ class ToolRegistry:
|
||||
resource = self._validate_resource(resource, UEX_DELETE_RESOURCES)
|
||||
return self._pending(label or f"DELETE {resource}", resource, params or {}, method="DELETE")
|
||||
|
||||
async def search_uex_api_index(
|
||||
self,
|
||||
query: str = "",
|
||||
group: str | None = None,
|
||||
history_only: bool = False,
|
||||
limit: int = 20,
|
||||
) -> dict[str, Any]:
|
||||
needle = query.casefold().strip()
|
||||
matches = []
|
||||
for resource, info in sorted(UEX_GET_RESOURCES.items()):
|
||||
if group and info["group"] != group:
|
||||
continue
|
||||
if history_only and not info.get("history"):
|
||||
continue
|
||||
haystack = " ".join(
|
||||
[
|
||||
resource,
|
||||
info["group"],
|
||||
" ".join(info["params"]),
|
||||
UEX_RESOURCE_DESCRIPTIONS.get(resource, ""),
|
||||
]
|
||||
).casefold()
|
||||
if needle and needle not in haystack:
|
||||
continue
|
||||
matches.append(self._resource_index_entry("GET", resource, info))
|
||||
if len(matches) >= max(1, min(limit, 50)):
|
||||
break
|
||||
|
||||
post_matches = []
|
||||
if not history_only:
|
||||
for resource in sorted(UEX_POST_RESOURCES):
|
||||
if group and group != "write":
|
||||
continue
|
||||
if needle and needle not in resource.casefold():
|
||||
continue
|
||||
post_matches.append(
|
||||
{
|
||||
"method": "POST",
|
||||
"resource": resource,
|
||||
"tool": self._post_tool_name(resource),
|
||||
"approval_required": True,
|
||||
"docs_url": self._docs_url("post", resource),
|
||||
}
|
||||
)
|
||||
|
||||
delete_matches = []
|
||||
if not history_only:
|
||||
for resource in sorted(UEX_DELETE_RESOURCES):
|
||||
if group and group != "write":
|
||||
continue
|
||||
if needle and needle not in resource.casefold():
|
||||
continue
|
||||
delete_matches.append(
|
||||
{
|
||||
"method": "DELETE",
|
||||
"resource": resource,
|
||||
"tool": self._delete_tool_name(resource),
|
||||
"approval_required": True,
|
||||
"docs_url": self._docs_url("delete", resource),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"count": len(matches) + len(post_matches) + len(delete_matches),
|
||||
"get": matches,
|
||||
"post": post_matches[: max(0, min(limit, 50) - len(matches))],
|
||||
"delete": delete_matches[: max(0, min(limit, 50) - len(matches) - len(post_matches))],
|
||||
}
|
||||
|
||||
async def summarize_uex_commodity_price_history(
|
||||
self,
|
||||
id_terminal: int,
|
||||
id_commodity: int,
|
||||
game_version: str | None = None,
|
||||
limit: int = 100,
|
||||
) -> dict[str, Any]:
|
||||
return await self._history_summary(
|
||||
"commodities_prices_history",
|
||||
{"id_terminal": id_terminal, "id_commodity": id_commodity, "game_version": game_version},
|
||||
value_fields=["price_buy", "price_sell", "scu_buy", "scu_sell", "scu_sell_stock"],
|
||||
label_fields=["commodity_name", "terminal_name", "game_version"],
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
async def summarize_uex_marketplace_price_history(
|
||||
self,
|
||||
id_item: str | int | None = None,
|
||||
id_listing: int | None = None,
|
||||
id_terminal: int | None = None,
|
||||
id_star_system: int | None = None,
|
||||
id_category: int | None = None,
|
||||
item_uuid: str | None = None,
|
||||
item_name: str | None = None,
|
||||
operation: str | None = None,
|
||||
quality_tier: int | None = None,
|
||||
currency: str | None = None,
|
||||
game_version: str | None = None,
|
||||
date_start: str | None = None,
|
||||
date_end: str | None = None,
|
||||
limit: int = 250,
|
||||
) -> dict[str, Any]:
|
||||
params = {
|
||||
"id_item": id_item,
|
||||
"id_listing": id_listing,
|
||||
"id_terminal": id_terminal,
|
||||
"id_star_system": id_star_system,
|
||||
"id_category": id_category,
|
||||
"item_uuid": item_uuid,
|
||||
"item_name": item_name,
|
||||
"operation": operation,
|
||||
"quality_tier": quality_tier,
|
||||
"currency": currency,
|
||||
"game_version": game_version,
|
||||
"date_start": date_start,
|
||||
"date_end": date_end,
|
||||
}
|
||||
return await self._history_summary(
|
||||
"marketplace_prices_history",
|
||||
params,
|
||||
value_fields=["price", "quality"],
|
||||
label_fields=["item_name", "operation", "currency", "terminal_name", "game_version"],
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
async def summarize_uex_currency_index_history(
|
||||
self,
|
||||
currency: str | None = None,
|
||||
date_from: int | None = None,
|
||||
date_to: int | None = None,
|
||||
limit: int = 365,
|
||||
) -> dict[str, Any]:
|
||||
return await self._history_summary(
|
||||
"currencies_index_history",
|
||||
{"currency": currency, "date_from": date_from, "date_to": date_to},
|
||||
value_fields=["index_value", "basket_value", "data_window_days"],
|
||||
label_fields=["currency", "methodology"],
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
def _make_get_handler(self, resource: str) -> ToolHandler:
|
||||
async def handler(**arguments: Any) -> dict[str, Any]:
|
||||
fields = arguments.pop("fields", None)
|
||||
@@ -502,6 +672,95 @@ class ToolRegistry:
|
||||
for resource, info in sorted(UEX_GET_RESOURCES.items())
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _api_index_schema(cls) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_uex_api_index",
|
||||
"description": "Search the indexed UEX API tool catalog by topic, resource, parameter, or group. Use to discover exact tool names, especially history tools.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string"},
|
||||
"group": {
|
||||
"type": "string",
|
||||
"enum": ["trade", "marketplace", "items", "vehicles", "locations", "mining", "user", "reference", "data", "write"],
|
||||
},
|
||||
"history_only": {"type": "boolean", "default": False},
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 50, "default": 20},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _history_summary_schemas(cls) -> list[dict[str, Any]]:
|
||||
controls = {
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 1000, "default": 250},
|
||||
}
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "summarize_uex_commodity_price_history",
|
||||
"description": "Summarize historical commodity price and inventory changes for one commodity at one terminal.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"required": ["id_terminal", "id_commodity"],
|
||||
"properties": {
|
||||
"id_terminal": {"type": "integer"},
|
||||
"id_commodity": {"type": "integer"},
|
||||
"game_version": {"type": "string"},
|
||||
**controls,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "summarize_uex_marketplace_price_history",
|
||||
"description": "Summarize marketplace historical price snapshots for an item, listing, terminal, category, system, or date range.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id_item": {"oneOf": [{"type": "integer"}, {"type": "string"}]},
|
||||
"id_listing": {"type": "integer"},
|
||||
"id_terminal": {"type": "integer"},
|
||||
"id_star_system": {"type": "integer"},
|
||||
"id_category": {"type": "integer"},
|
||||
"item_uuid": {"type": "string"},
|
||||
"item_name": {"type": "string"},
|
||||
"operation": {"type": "string", "enum": ["buy", "sell"]},
|
||||
"quality_tier": {"type": "integer", "minimum": 0, "maximum": 4},
|
||||
"currency": {"type": "string"},
|
||||
"game_version": {"type": "string"},
|
||||
"date_start": {"type": "string", "description": "YYYY-MM-DD"},
|
||||
"date_end": {"type": "string", "description": "YYYY-MM-DD"},
|
||||
**controls,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "summarize_uex_currency_index_history",
|
||||
"description": "Summarize historical UEX currency index snapshots and basket value changes.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"currency": {"type": "string"},
|
||||
"date_from": {"type": "integer", "description": "Unix timestamp."},
|
||||
"date_to": {"type": "integer", "description": "Unix timestamp."},
|
||||
**controls,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _uex_post_schemas(cls) -> list[dict[str, Any]]:
|
||||
return [
|
||||
@@ -566,7 +825,7 @@ class ToolRegistry:
|
||||
|
||||
@staticmethod
|
||||
def _query_param_schema(param: str) -> dict[str, Any]:
|
||||
if param == "id" or param.startswith("id_"):
|
||||
if param == "id" or param.startswith("id_") or param in {"date_from", "date_to", "quality_tier"}:
|
||||
return {"type": "integer"}
|
||||
return {"type": "string"}
|
||||
|
||||
@@ -574,7 +833,11 @@ class ToolRegistry:
|
||||
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}"
|
||||
history = " History endpoint." if info.get("history") else ""
|
||||
description = UEX_RESOURCE_DESCRIPTIONS.get(resource)
|
||||
if description:
|
||||
return f"GET UEX /{resource}/ with compact, token-limited results. {description}{auth}{heavy}"
|
||||
return f"GET UEX /{resource}/ with compact, token-limited results.{history}{auth}{heavy}"
|
||||
|
||||
@staticmethod
|
||||
def _get_tool_name(resource: str) -> str:
|
||||
@@ -588,6 +851,25 @@ class ToolRegistry:
|
||||
def _delete_tool_name(resource: str) -> str:
|
||||
return f"delete_uex_{resource}"
|
||||
|
||||
@classmethod
|
||||
def _resource_index_entry(cls, method: str, resource: str, info: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"method": method,
|
||||
"resource": resource,
|
||||
"tool": cls._get_tool_name(resource),
|
||||
"group": info["group"],
|
||||
"params": info["params"],
|
||||
"authenticated": info["auth"],
|
||||
"history": bool(info.get("history")),
|
||||
"heavy": bool(info.get("heavy")),
|
||||
"description": UEX_RESOURCE_DESCRIPTIONS.get(resource, ""),
|
||||
"docs_url": cls._docs_url("get", resource),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _docs_url(method: str, resource: str) -> str:
|
||||
return f"https://uexcorp.space/api/documentation/id/{method}_{resource}/"
|
||||
|
||||
async def search_marketplace_listings(
|
||||
self,
|
||||
query: str | None = None,
|
||||
@@ -792,6 +1074,82 @@ class ToolRegistry:
|
||||
return " ".join(str(value) for value in item.values() if isinstance(value, (str, int, float))).casefold()
|
||||
return str(item).casefold()
|
||||
|
||||
async def _history_summary(
|
||||
self,
|
||||
resource: str,
|
||||
params: dict[str, Any],
|
||||
value_fields: list[str],
|
||||
label_fields: list[str],
|
||||
limit: int,
|
||||
) -> dict[str, Any]:
|
||||
info = UEX_GET_RESOURCES[resource]
|
||||
cleaned_params = self._filter_params(params, info["params"])
|
||||
response = await self.uex.get(resource, cleaned_params, authenticated=bool(info["auth"]))
|
||||
rows = [
|
||||
row
|
||||
for row in self._as_list(response.get("data"))
|
||||
if isinstance(row, dict)
|
||||
][: max(1, min(limit, 1000))]
|
||||
rows_sorted = sorted(rows, key=lambda row: int(row.get("date_added") or 0))
|
||||
latest = rows_sorted[-1] if rows_sorted else {}
|
||||
earliest = rows_sorted[0] if rows_sorted else {}
|
||||
summaries = {
|
||||
field: self._numeric_history_summary(rows_sorted, field)
|
||||
for field in value_fields
|
||||
if any(self._is_number(row.get(field)) for row in rows_sorted)
|
||||
}
|
||||
labels = {
|
||||
field: latest.get(field)
|
||||
for field in label_fields
|
||||
if latest.get(field) not in (None, "")
|
||||
}
|
||||
sample_fields = ["id", "date_added", *label_fields, *value_fields]
|
||||
recent = [
|
||||
self._project_item(row, fields=sample_fields, mode="summary")
|
||||
for row in list(reversed(rows_sorted[-5:]))
|
||||
]
|
||||
return {
|
||||
"status": response.get("status"),
|
||||
"resource": resource,
|
||||
"params": cleaned_params,
|
||||
"count": len(rows),
|
||||
"date_start": earliest.get("date_added"),
|
||||
"date_end": latest.get("date_added"),
|
||||
"labels": labels,
|
||||
"metrics": summaries,
|
||||
"recent": recent,
|
||||
"docs_url": self._docs_url("get", resource),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _numeric_history_summary(cls, rows: list[dict[str, Any]], field: str) -> dict[str, Any]:
|
||||
points = [
|
||||
(int(row.get("date_added") or 0), float(row[field]))
|
||||
for row in rows
|
||||
if cls._is_number(row.get(field))
|
||||
]
|
||||
values = [value for _, value in points]
|
||||
first_date, first_value = points[0]
|
||||
last_date, last_value = points[-1]
|
||||
change = last_value - first_value
|
||||
pct_change = (change / first_value * 100) if first_value else None
|
||||
return {
|
||||
"first": first_value,
|
||||
"first_date": first_date,
|
||||
"latest": last_value,
|
||||
"latest_date": last_date,
|
||||
"min": min(values),
|
||||
"max": max(values),
|
||||
"avg": round(sum(values) / len(values), 4),
|
||||
"change": round(change, 4),
|
||||
"pct_change": round(pct_change, 4) if pct_change is not None else None,
|
||||
"points": len(points),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _is_number(value: Any) -> bool:
|
||||
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
||||
|
||||
@staticmethod
|
||||
def _summarize_listing(listing: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user