feat: history tools
This commit is contained in:
@@ -8,6 +8,52 @@ from traderai.uex_client import UEXClient
|
|||||||
|
|
||||||
class FakeUEX:
|
class FakeUEX:
|
||||||
async def get(self, path, params=None, authenticated=False):
|
async def get(self, path, params=None, authenticated=False):
|
||||||
|
if path == "commodities_prices_history":
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"id_terminal": 7,
|
||||||
|
"id_commodity": 3,
|
||||||
|
"commodity_name": "Gold",
|
||||||
|
"terminal_name": "Port Tressler",
|
||||||
|
"price_buy": 4000,
|
||||||
|
"price_sell": 5000,
|
||||||
|
"scu_buy": 100,
|
||||||
|
"scu_sell": 20,
|
||||||
|
"date_added": 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"id_terminal": 7,
|
||||||
|
"id_commodity": 3,
|
||||||
|
"commodity_name": "Gold",
|
||||||
|
"terminal_name": "Port Tressler",
|
||||||
|
"price_buy": 4200,
|
||||||
|
"price_sell": 4800,
|
||||||
|
"scu_buy": 80,
|
||||||
|
"scu_sell": 30,
|
||||||
|
"date_added": 200,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
if path == "marketplace_prices_history":
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"data": [
|
||||||
|
{"id": 1, "item_name": "Widget", "operation": "sell", "price": 1000, "currency": "UEC", "date_added": 100},
|
||||||
|
{"id": 2, "item_name": "Widget", "operation": "sell", "price": 1250, "currency": "UEC", "date_added": 200},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
if path == "currencies_index_history":
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"data": [
|
||||||
|
{"id": 1, "currency": "UEC", "index_value": 100.0, "basket_value": 5000.0, "date_added": 100},
|
||||||
|
{"id": 2, "currency": "UEC", "index_value": 110.0, "basket_value": 5500.0, "date_added": 200},
|
||||||
|
],
|
||||||
|
}
|
||||||
if path == "commodities_prices":
|
if path == "commodities_prices":
|
||||||
return {
|
return {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
@@ -170,6 +216,45 @@ def test_schemas_expose_specific_uex_tools_instead_of_generic_api_tool():
|
|||||||
assert "uex_draft_post" not in names
|
assert "uex_draft_post" not in names
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_search_uex_api_index_finds_history_tools():
|
||||||
|
registry = ToolRegistry(FakeUEX())
|
||||||
|
|
||||||
|
result = await registry.execute("search_uex_api_index", {"query": "history", "history_only": True})
|
||||||
|
|
||||||
|
tools = {item["tool"] for item in result["get"]}
|
||||||
|
assert "get_uex_commodities_prices_history" in tools
|
||||||
|
assert "get_uex_marketplace_prices_history" in tools
|
||||||
|
assert "get_uex_currencies_index_history" in tools
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_summarize_commodity_price_history_returns_trend_metrics():
|
||||||
|
registry = ToolRegistry(FakeUEX())
|
||||||
|
|
||||||
|
result = await registry.execute(
|
||||||
|
"summarize_uex_commodity_price_history",
|
||||||
|
{"id_terminal": 7, "id_commodity": 3},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["resource"] == "commodities_prices_history"
|
||||||
|
assert result["count"] == 2
|
||||||
|
assert result["labels"] == {"commodity_name": "Gold", "terminal_name": "Port Tressler"}
|
||||||
|
assert result["metrics"]["price_buy"]["change"] == 200
|
||||||
|
assert result["metrics"]["price_sell"]["pct_change"] == -4.0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_summarize_marketplace_and_currency_history():
|
||||||
|
registry = ToolRegistry(FakeUEX())
|
||||||
|
|
||||||
|
market = await registry.execute("summarize_uex_marketplace_price_history", {"item_name": "Widget"})
|
||||||
|
currency = await registry.execute("summarize_uex_currency_index_history", {"currency": "UEC"})
|
||||||
|
|
||||||
|
assert market["metrics"]["price"]["pct_change"] == 25.0
|
||||||
|
assert currency["metrics"]["index_value"]["change"] == 10.0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@respx.mock
|
@respx.mock
|
||||||
async def test_uex_client_get_user_normalizes_user_payload():
|
async def test_uex_client_get_user_normalizes_user_payload():
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ 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 UEX data, open/current listings, active negotiations, unread notifications, messages, offers, or posting ads.
|
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.
|
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.
|
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.
|
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.
|
||||||
@@ -280,6 +281,10 @@ class OllamaAgent:
|
|||||||
if name.startswith("delete_uex_"):
|
if name.startswith("delete_uex_"):
|
||||||
return f"Drafting UEX {name.removeprefix('delete_uex_')} delete for approval"
|
return f"Drafting UEX {name.removeprefix('delete_uex_')} delete for approval"
|
||||||
labels = {
|
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_api_catalog": "Checking UEX API catalog",
|
||||||
"uex_get": "Fetching UEX data",
|
"uex_get": "Fetching UEX data",
|
||||||
"uex_draft_post": "Drafting UEX write for approval",
|
"uex_draft_post": "Drafting UEX write for approval",
|
||||||
|
|||||||
+363
-5
@@ -25,7 +25,7 @@ UEX_GET_RESOURCES: dict[str, dict[str, Any]] = {
|
|||||||
"group": "trade",
|
"group": "trade",
|
||||||
},
|
},
|
||||||
"commodities_prices_all": {"params": [], "auth": False, "group": "trade", "heavy": True},
|
"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_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_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": {"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"},
|
"contracts": {"params": ["id", "name", "slug"], "auth": False, "group": "reference"},
|
||||||
"crew": {"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": {"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_extract": {"params": ["table"], "auth": False, "group": "data"},
|
||||||
"data_parameters": {"params": ["endpoint"], "auth": False, "group": "data"},
|
"data_parameters": {"params": ["endpoint"], "auth": False, "group": "data"},
|
||||||
"factions": {"params": ["id", "name", "slug"], "auth": False, "group": "reference"},
|
"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_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": {"params": ["id_item", "item_name", "item_slug"], "auth": False, "group": "marketplace"},
|
||||||
"marketplace_prices_averages_all": {"params": [], "auth": False, "group": "marketplace", "heavy": True},
|
"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"},
|
"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"},
|
"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": {"params": ["id", "id_star_system", "name", "slug"], "auth": False, "group": "locations"},
|
||||||
@@ -107,6 +126,12 @@ UEX_DELETE_RESOURCES = {
|
|||||||
"user_trades_remove",
|
"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
|
@dataclass
|
||||||
class PendingAction:
|
class PendingAction:
|
||||||
@@ -147,6 +172,10 @@ class ToolRegistry:
|
|||||||
self.handlers["uex_get"] = self.uex_get
|
self.handlers["uex_get"] = self.uex_get
|
||||||
self.handlers["uex_draft_post"] = self.uex_draft_post
|
self.handlers["uex_draft_post"] = self.uex_draft_post
|
||||||
self.handlers["uex_draft_delete"] = self.uex_draft_delete
|
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:
|
for resource in UEX_GET_RESOURCES:
|
||||||
self.handlers[self._get_tool_name(resource)] = self._make_get_handler(resource)
|
self.handlers[self._get_tool_name(resource)] = self._make_get_handler(resource)
|
||||||
for resource in UEX_POST_RESOURCES:
|
for resource in UEX_POST_RESOURCES:
|
||||||
@@ -157,7 +186,9 @@ class ToolRegistry:
|
|||||||
@property
|
@property
|
||||||
def schemas(self) -> list[dict[str, Any]]:
|
def schemas(self) -> list[dict[str, Any]]:
|
||||||
return [
|
return [
|
||||||
|
self._api_index_schema(),
|
||||||
*self._uex_get_schemas(),
|
*self._uex_get_schemas(),
|
||||||
|
*self._history_summary_schemas(),
|
||||||
*self._uex_post_schemas(),
|
*self._uex_post_schemas(),
|
||||||
*self._uex_delete_schemas(),
|
*self._uex_delete_schemas(),
|
||||||
{
|
{
|
||||||
@@ -457,6 +488,145 @@ class ToolRegistry:
|
|||||||
resource = self._validate_resource(resource, UEX_DELETE_RESOURCES)
|
resource = self._validate_resource(resource, UEX_DELETE_RESOURCES)
|
||||||
return self._pending(label or f"DELETE {resource}", resource, params or {}, method="DELETE")
|
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:
|
def _make_get_handler(self, resource: str) -> ToolHandler:
|
||||||
async def handler(**arguments: Any) -> dict[str, Any]:
|
async def handler(**arguments: Any) -> dict[str, Any]:
|
||||||
fields = arguments.pop("fields", None)
|
fields = arguments.pop("fields", None)
|
||||||
@@ -502,6 +672,95 @@ class ToolRegistry:
|
|||||||
for resource, info in sorted(UEX_GET_RESOURCES.items())
|
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
|
@classmethod
|
||||||
def _uex_post_schemas(cls) -> list[dict[str, Any]]:
|
def _uex_post_schemas(cls) -> list[dict[str, Any]]:
|
||||||
return [
|
return [
|
||||||
@@ -566,7 +825,7 @@ class ToolRegistry:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _query_param_schema(param: str) -> dict[str, Any]:
|
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": "integer"}
|
||||||
return {"type": "string"}
|
return {"type": "string"}
|
||||||
|
|
||||||
@@ -574,7 +833,11 @@ class ToolRegistry:
|
|||||||
def _get_tool_description(resource: str, info: dict[str, Any]) -> str:
|
def _get_tool_description(resource: str, info: dict[str, Any]) -> str:
|
||||||
auth = " Authenticated." if info["auth"] else ""
|
auth = " Authenticated." if info["auth"] else ""
|
||||||
heavy = " Heavy endpoint; use fields and limit." if info.get("heavy") 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
|
@staticmethod
|
||||||
def _get_tool_name(resource: str) -> str:
|
def _get_tool_name(resource: str) -> str:
|
||||||
@@ -588,6 +851,25 @@ class ToolRegistry:
|
|||||||
def _delete_tool_name(resource: str) -> str:
|
def _delete_tool_name(resource: str) -> str:
|
||||||
return f"delete_uex_{resource}"
|
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(
|
async def search_marketplace_listings(
|
||||||
self,
|
self,
|
||||||
query: str | None = None,
|
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 " ".join(str(value) for value in item.values() if isinstance(value, (str, int, float))).casefold()
|
||||||
return str(item).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
|
@staticmethod
|
||||||
def _summarize_listing(listing: dict[str, Any]) -> dict[str, Any]:
|
def _summarize_listing(listing: dict[str, Any]) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user