versioning: 0.0.4, feat: create listing, source image
Build Release EXE / build-windows-exe (release) Successful in 52s

This commit is contained in:
2026-05-08 00:02:59 -04:00
parent e2f87481d6
commit 97c751c585
7 changed files with 297 additions and 12 deletions
+4 -1
View File
@@ -20,6 +20,7 @@ Use the specific UEX tool for the needed endpoint, such as get_uex_commodities_p
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.
Use SCMDB tools when the user asks about Star Citizen missions/contracts, mission rewards, payouts, reputation gains, item rewards, blueprint rewards, or hauling mission cargo. Prefer SCMDB live data unless the user asks for PTU or a specific game version.
Use Cornerstone tools when the user asks where an item is sold, which shops carry an item, item store locations, in-game item base prices, or Universal Item Finder data.
When drafting UEX marketplace item posts that need images, use Cornerstone media tools or draft_marketplace_listing_with_cornerstone_image so the pending listing can include UEX image_data sourced from Cornerstone.
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.
@@ -474,7 +475,7 @@ class OllamaAgent:
"label": action.label,
"method": action.method,
"endpoint": action.endpoint,
"payload": action.payload,
"payload": self.tools._display_payload(action.payload) if hasattr(self.tools, "_display_payload") else action.payload,
"metadata": action.metadata or {},
}
for action in self.tools.pending_actions.values()
@@ -524,6 +525,7 @@ class OllamaAgent:
"get_scmdb_mission_rewards": "Fetching SCMDB mission rewards",
"search_cornerstone_items": "Searching Cornerstone items",
"get_cornerstone_item_locations": "Fetching Cornerstone item locations",
"get_cornerstone_item_media": "Fetching Cornerstone item media",
"uex_api_catalog": "Checking UEX API catalog",
"uex_get": "Fetching UEX data",
"uex_draft_post": "Drafting UEX write for approval",
@@ -534,6 +536,7 @@ class OllamaAgent:
"get_negotiation_messages": "Reading negotiation messages",
"draft_negotiation_message": "Drafting message for approval",
"draft_marketplace_listing": "Drafting listing for approval",
"draft_marketplace_listing_with_cornerstone_image": "Drafting listing with Cornerstone image",
"check_uex_notifications": "Checking UEX notifications",
}
return labels.get(name, f"Running {name}")
+68 -1
View File
@@ -1,8 +1,10 @@
from __future__ import annotations
from html.parser import HTMLParser
import base64
import json
from typing import Any
from urllib.parse import urljoin
import httpx
@@ -41,6 +43,23 @@ class CornerstoneClient:
raise CornerstoneError(f"Cornerstone HTTP {response.status_code}: {response.text[:240]}")
return {"url": str(response.url), "html": response.text}
async def get_image_data(self, url: str, max_bytes: int = 10_000_000) -> dict[str, Any]:
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
response = await client.get(url, headers={"Accept": "image/png,image/jpeg,image/*"})
if response.status_code >= 400:
raise CornerstoneError(f"Cornerstone image HTTP {response.status_code}: {response.text[:240]}")
content_type = response.headers.get("content-type", "").split(";")[0].strip().casefold()
if content_type not in {"image/jpeg", "image/jpg", "image/png"}:
raise CornerstoneError(f"Cornerstone image was not JPG or PNG: {content_type or 'unknown content type'}")
if len(response.content) > max_bytes:
raise CornerstoneError(f"Cornerstone image is larger than {max_bytes} bytes.")
return {
"url": str(response.url),
"content_type": content_type,
"size_bytes": len(response.content),
"image_data": base64.b64encode(response.content).decode("ascii"),
}
async def _get_json(self, path: str) -> Any:
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
response = await client.get(f"{self.base_url}/{path.lstrip('/')}", headers={"Accept": "application/json"})
@@ -58,6 +77,7 @@ class CornerstonePageParser(HTMLParser):
super().__init__(convert_charrefs=True)
self.title = ""
self.tables: list[list[list[str]]] = []
self.images: list[dict[str, str]] = []
self._skip_depth = 0
self._in_title = False
self._current_table: list[list[str]] | None = None
@@ -73,6 +93,29 @@ class CornerstonePageParser(HTMLParser):
return
if tag == "title":
self._in_title = True
elif tag == "meta":
attr_map = self._attrs(attrs)
name = (attr_map.get("property") or attr_map.get("name") or "").casefold()
content = attr_map.get("content") or ""
if content and name in {"og:image", "twitter:image", "twitter:image:src"}:
self.images.append({"url": content, "source": name})
elif tag == "link":
attr_map = self._attrs(attrs)
rel = (attr_map.get("rel") or "").casefold()
href = attr_map.get("href") or ""
if href and "image_src" in rel:
self.images.append({"url": href, "source": "link:image_src"})
elif tag == "img":
attr_map = self._attrs(attrs)
url = attr_map.get("src") or attr_map.get("data-src") or attr_map.get("data-original") or ""
if url:
self.images.append(
{
"url": url,
"alt": attr_map.get("alt") or "",
"source": "img",
}
)
elif tag == "table":
self._current_table = []
elif tag == "tr" and self._current_table is not None:
@@ -110,8 +153,12 @@ class CornerstonePageParser(HTMLParser):
if self._current_cell is not None:
self._current_cell.append(data)
@staticmethod
def _attrs(attrs: list[tuple[str, str | None]]) -> dict[str, str]:
return {key.casefold(): value or "" for key, value in attrs}
def parse_cornerstone_item_page(html: str) -> dict[str, Any]:
def parse_cornerstone_item_page(html: str, page_url: str | None = None) -> dict[str, Any]:
parser = CornerstonePageParser()
parser.feed(html)
info: dict[str, Any] = {"page_title": " ".join(parser.title.split())}
@@ -142,6 +189,9 @@ def parse_cornerstone_item_page(html: str) -> dict[str, Any]:
general[key] = value
info["name"] = general.get("name") or _name_from_title(info["page_title"])
media = _dedupe_media(parser.images, page_url)
if media:
info["media"] = media
if general:
info["general"] = general
info["locations"] = locations
@@ -157,3 +207,20 @@ def _name_from_title(title: str) -> str | None:
if " - " not in title:
return title or None
return title.rsplit(" - ", 1)[-1].strip() or None
def _dedupe_media(images: list[dict[str, str]], page_url: str | None = None) -> list[dict[str, str]]:
media = []
seen = set()
for image in images:
raw_url = (image.get("url") or "").strip()
if not raw_url or raw_url.startswith("data:"):
continue
url = urljoin(page_url or "", raw_url)
if url in seen:
continue
seen.add(url)
item = dict(image)
item["url"] = url
media.append(item)
return media
+156 -4
View File
@@ -196,6 +196,8 @@ class ToolRegistry:
"get_scmdb_mission_rewards": self.get_scmdb_mission_rewards,
"search_cornerstone_items": self.search_cornerstone_items,
"get_cornerstone_item_locations": self.get_cornerstone_item_locations,
"get_cornerstone_item_media": self.get_cornerstone_item_media,
"draft_marketplace_listing_with_cornerstone_image": self.draft_marketplace_listing_with_cornerstone_image,
}
self.handlers["uex_api_catalog"] = self.uex_api_catalog
self.handlers["uex_get"] = self.uex_get
@@ -311,7 +313,7 @@ class ToolRegistry:
"type": "function",
"function": {
"name": "draft_marketplace_listing",
"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.",
"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. Prefer draft_marketplace_listing_with_cornerstone_image for item posts when a Cornerstone image is useful.",
"parameters": {
"type": "object",
"required": ["id_category", "operation", "type", "unit", "title", "description", "price", "currency", "language"],
@@ -332,8 +334,12 @@ class ToolRegistry:
"source": {"type": "string"},
"availability": {"type": "string"},
"in_stock": {"type": "integer"},
"durability": {"type": "integer", "minimum": 0, "maximum": 100},
"video_url": {"type": "string"},
"image_data": {"type": "string", "description": "Base64 JPG or PNG image data for UEX upload."},
"hours_expiration": {"type": "integer"},
"is_hidden": {"type": "integer", "enum": [0, 1]},
"is_tv_allowed": {"type": "integer", "enum": [0, 1]},
"is_production": {"type": "integer", "enum": [0, 1], "default": 1},
},
},
@@ -512,7 +518,7 @@ class ToolRegistry:
"label": action.label,
"method": action.method,
"endpoint": action.endpoint,
"payload": action.payload,
"payload": self._display_payload(action.payload),
"metadata": action.metadata or {},
},
}
@@ -973,6 +979,60 @@ class ToolRegistry:
},
},
},
{
"type": "function",
"function": {
"name": "get_cornerstone_item_media",
"description": "Fetch Cornerstone item page media, especially image URLs that can be used when drafting UEX marketplace listings.",
"parameters": {
"type": "object",
"properties": {
"id": {"type": "string", "description": "Cornerstone item id from search_cornerstone_items."},
"query": {"type": "string", "description": "Item name if id is not known."},
"limit": {"type": "integer", "minimum": 1, "maximum": 10, "default": 5},
},
},
},
},
{
"type": "function",
"function": {
"name": "draft_marketplace_listing_with_cornerstone_image",
"description": "Draft a UEX marketplace listing and source the listing image from Cornerstone. The image is downloaded as base64 image_data and included in the pending action. Nothing is posted until user approval.",
"parameters": {
"type": "object",
"required": ["item_query", "id_category", "operation", "type", "unit", "title", "description", "price", "currency", "language"],
"properties": {
"item_query": {"type": "string", "description": "Cornerstone item name to source an image from."},
"cornerstone_id": {"type": "string", "description": "Cornerstone item id, if already known."},
"id_item": {"type": "integer"},
"id_star_system": {"type": "integer"},
"id_terminal": {"type": "integer"},
"id_organization": {"type": "integer"},
"id_category": {"type": "integer"},
"operation": {"type": "string", "enum": ["buy", "sell", "rent", "trade"]},
"type": {"type": "string", "enum": ["item", "service", "contract"]},
"unit": {"type": "string"},
"title": {"type": "string"},
"description": {"type": "string"},
"price": {"type": "number"},
"currency": {"type": "string", "enum": ["UEC"]},
"language": {"type": "string", "default": "en_US"},
"location": {"type": "string"},
"source": {"type": "string", "enum": ["looted", "pledged", "purchased_in_game", "pirated", "gifted"]},
"availability": {"type": "string"},
"in_stock": {"type": "integer"},
"durability": {"type": "integer", "minimum": 0, "maximum": 100},
"video_url": {"type": "string"},
"hours_expiration": {"type": "integer"},
"is_hidden": {"type": "integer", "enum": [0, 1]},
"is_tv_allowed": {"type": "integer", "enum": [0, 1]},
"is_production": {"type": "integer", "enum": [0, 1], "default": 1},
"require_image": {"type": "boolean", "default": False, "description": "Return an error instead of drafting if no Cornerstone JPG/PNG image can be sourced."},
},
},
},
},
]
@classmethod
@@ -1167,6 +1227,55 @@ class ToolRegistry:
async def draft_marketplace_listing(self, **payload: Any) -> dict[str, Any]:
return self._pending("Post marketplace listing", "marketplace_advertise", payload)
async def draft_marketplace_listing_with_cornerstone_image(
self,
item_query: str,
cornerstone_id: str | None = None,
**payload: Any,
) -> dict[str, Any]:
require_image = bool(payload.pop("require_image", False))
item = await self._resolve_cornerstone_item(id=cornerstone_id, query=item_query)
if not item:
return {"error": "No Cornerstone item matched. Provide cornerstone_id or a more specific item_query."}
page = await self.cornerstone.get_item_page(str(item["id"]))
parsed = parse_cornerstone_item_page(page["html"], page["url"])
media = parsed.get("media") or []
image_result: dict[str, Any] | None = None
image_error = ""
for media_item in media:
try:
image_result = await self.cornerstone.get_image_data(media_item["url"])
break
except Exception as exc:
image_error = str(exc)
if image_result:
payload["image_data"] = image_result["image_data"]
elif require_image:
return {
"error": "Cornerstone item matched, but no usable JPG/PNG image could be sourced.",
"cornerstone": {
"item": {"id": item.get("id"), "name": parsed.get("name") or item.get("name")},
"url": page["url"],
"media": media,
"image_error": image_error,
},
}
payload.setdefault("id_item", self._int_or_none(item.get("id")))
metadata = {
"cornerstone_item_id": item.get("id"),
"cornerstone_item_name": parsed.get("name") or item.get("name"),
"cornerstone_url": page["url"],
"cornerstone_image_url": image_result.get("url") if image_result else None,
"cornerstone_image_content_type": image_result.get("content_type") if image_result else None,
"cornerstone_image_size_bytes": image_result.get("size_bytes") if image_result else None,
"cornerstone_image_status": "included" if image_result else "not_found",
"cornerstone_image_error": image_error or None,
}
return self._pending("Post marketplace listing with Cornerstone image", "marketplace_advertise", payload, metadata=metadata)
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."}
@@ -1430,7 +1539,7 @@ class ToolRegistry:
return {"error": "No Cornerstone item matched. Provide an id or a more specific query."}
page = await self.cornerstone.get_item_page(str(item["id"]))
parsed = parse_cornerstone_item_page(page["html"])
parsed = parse_cornerstone_item_page(page["html"], page["url"])
locations = parsed.get("locations") or []
location_filter = (location or "").casefold().strip()
if location_filter:
@@ -1455,6 +1564,34 @@ class ToolRegistry:
"locations": locations[:limit],
}
async def get_cornerstone_item_media(
self,
id: str | None = None,
query: str | None = None,
limit: int = 5,
) -> dict[str, Any]:
item = await self._resolve_cornerstone_item(id=id, query=query)
if not item:
return {"error": "No Cornerstone item matched. Provide an id or a more specific query."}
page = await self.cornerstone.get_item_page(str(item["id"]))
parsed = parse_cornerstone_item_page(page["html"], page["url"])
media = parsed.get("media") or []
limit = max(1, min(limit, 10))
return {
"source": self.cornerstone.base_url,
"url": page["url"],
"item": {
"id": item.get("id"),
"name": parsed.get("name") or item.get("name"),
"sold": bool(item.get("sold")),
"general": parsed.get("general") or {},
},
"returned": min(len(media), limit),
"truncated": len(media) > limit,
"media": media[:limit],
}
def _pending(
self,
label: str,
@@ -1474,12 +1611,27 @@ class ToolRegistry:
"label": label,
"method": method,
"endpoint": endpoint,
"payload": payload,
"payload": self._display_payload(payload),
"metadata": metadata,
"approval_required": self.require_write_approval,
}
}
@staticmethod
def _display_payload(payload: dict[str, Any]) -> dict[str, Any]:
display = dict(payload)
image_data = display.get("image_data")
if isinstance(image_data, str) and image_data:
display["image_data"] = f"<base64 image data redacted; {len(image_data)} characters>"
return display
@staticmethod
def _int_or_none(value: Any) -> int | None:
try:
return int(value)
except (TypeError, ValueError):
return None
def _record_pending_action_result(self, action: PendingAction, result_kind: str, result: dict[str, Any]) -> None:
metadata = action.metadata or {}
plan_id = metadata.get("plan_id")
+2 -1
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
__version__ = "0.0.3"
__version__ = "0.0.4"
RELEASES_URL = "https://git.hudsonriggs.systems/LambdaBankingConglomerate/TraderAI/releases"
RELEASES_API_URL = "https://git.hudsonriggs.systems/api/v1/repos/LambdaBankingConglomerate/TraderAI/releases"
@@ -9,3 +9,4 @@ RELEASES_API_URL = "https://git.hudsonriggs.systems/api/v1/repos/LambdaBankingCo