feat: infrance
Build Release EXE / build-windows-exe (release) Successful in 58s

This commit is contained in:
2026-06-08 20:28:06 -04:00
parent 6bd1e81a51
commit 00cf6f8747
20 changed files with 2789 additions and 180 deletions
+581 -7
View File
@@ -1,9 +1,15 @@
from __future__ import annotations
import asyncio
import json
import re
import shutil
import subprocess
import tempfile
import uuid
from collections.abc import AsyncIterator
from contextlib import nullcontext
from pathlib import Path
from typing import Any
import httpx
@@ -11,6 +17,7 @@ from tzlocal import get_localzone
from traderai.memory import DEFAULT_THREAD_ID, MemoryStore, iso_now, iso_now_in_zone, time_since
from traderai.tools import ToolRegistry
from traderai.version import __version__
SYSTEM_PROMPT = """You are TraderAI, a local assistant for UEX marketplace work.
@@ -19,7 +26,9 @@ Use continual plan tools when the user asks for multi-day or recurring marketpla
UEX credentials are configured server-side when available. Never ask the user to provide UEX_SECRET_KEY or UEX_BEARER_TOKEN in chat; call the authenticated UEX tool and only mention credential configuration if the tool returns an authentication error.
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.
When you need missing Star Citizen knowledge to answer accurately, use Star Citizen Wiki tools during your reasoning instead of guessing.
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 Star Citizen Wiki tools for general game knowledge, ships and vehicles, store availability, purchase locations, ship prices, manufacturers, locations, and page summaries from starcitizen.tools.
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.
@@ -41,6 +50,7 @@ class OllamaAgent:
num_ctx: int | None = None,
provider: str = "ollama",
api_key: str | None = None,
reasoning_effort: str = "medium",
) -> None:
self.base_url = base_url.rstrip("/")
self.model = model
@@ -50,11 +60,14 @@ class OllamaAgent:
self.num_ctx = num_ctx
self.provider = provider.strip().casefold() or "ollama"
self.api_key = api_key
self.reasoning_effort = reasoning_effort.strip().casefold() or "medium"
self.thread_messages: dict[str, list[dict[str, Any]]] = {}
async def health(self) -> dict[str, Any]:
if self.provider == "openai":
return await self._openai_health()
if self.provider == "codex":
return await self._codex_health()
try:
async with httpx.AsyncClient(timeout=3) as client:
response = await client.get(f"{self.base_url}/api/tags")
@@ -83,6 +96,8 @@ class OllamaAgent:
health = await self.health()
if not health["online"]:
raise OllamaUnavailable(health["message"])
if health.get("model_available") is False:
raise OllamaUnavailable(health["message"])
async def chat(
self,
@@ -304,6 +319,13 @@ class OllamaAgent:
previous_interaction=previous_interaction,
thread_id=thread_id,
)
if self.provider == "codex":
return await self._codex_chat(
query,
messages,
previous_interaction=previous_interaction,
thread_id=thread_id,
)
return await self._ollama_chat(
query,
messages,
@@ -327,6 +349,15 @@ class OllamaAgent:
):
yield event
return
if self.provider == "codex":
async for event in self._codex_chat_stream(
query,
messages,
previous_interaction=previous_interaction,
thread_id=thread_id,
):
yield event
return
async for event in self._ollama_chat_stream(
query,
messages,
@@ -410,6 +441,7 @@ class OllamaAgent:
thread_id=thread_id,
),
"tools": self.tools.schemas,
"reasoning_effort": self.reasoning_effort,
"stream": False,
},
)
@@ -447,6 +479,7 @@ class OllamaAgent:
thread_id=thread_id,
),
"tools": self.tools.schemas,
"reasoning_effort": self.reasoning_effort,
"stream": True,
},
) as response:
@@ -487,6 +520,47 @@ class OllamaAgent:
"done": True,
}
async def _codex_chat(
self,
query: str = "",
messages: list[dict[str, Any]] | None = None,
previous_interaction: dict[str, Any] | None = None,
thread_id: str | None = DEFAULT_THREAD_ID,
) -> dict[str, Any]:
result = await self._codex_cli_turn(
query,
messages or self._messages_for_thread(thread_id),
previous_interaction=previous_interaction,
thread_id=thread_id,
)
return self._codex_structured_response(result)
async def _codex_chat_stream(
self,
query: str = "",
messages: list[dict[str, Any]] | None = None,
previous_interaction: dict[str, Any] | None = None,
thread_id: str | None = DEFAULT_THREAD_ID,
) -> AsyncIterator[dict[str, Any]]:
result = await self._codex_cli_turn(
query,
messages or self._messages_for_thread(thread_id),
previous_interaction=previous_interaction,
thread_id=thread_id,
)
response = self._codex_structured_response(result)
message = response["message"]
if message.get("content"):
yield {"message": {"role": "assistant", "content": message["content"]}}
yield {
"message": {
"role": "assistant",
"content": "",
"tool_calls": message.get("tool_calls") or [],
},
"done": True,
}
def _messages_with_context(
self,
query: str,
@@ -511,15 +585,57 @@ class OllamaAgent:
return [messages[0], {"role": "system", "content": context}, *messages[1:]]
async def _openai_health(self) -> dict[str, Any]:
return await self._cloud_health("openai")
async def _codex_health(self) -> dict[str, Any]:
command = self._codex_command()
if not command:
return {
"online": False,
"model": self.model,
"base_url": self.base_url,
"provider": "codex",
"model_available": False,
"models": [],
"message": "Codex CLI was not found on PATH.",
"detail": "",
}
try:
account, models = await self._codex_app_server_status()
except Exception as exc:
return {
"online": False,
"model": self.model,
"base_url": command,
"provider": "codex",
"model_available": False,
"models": [],
"message": "Codex App Server is installed, but TraderAI could not connect to it.",
"detail": str(exc),
}
logged_in = bool(account)
detail = f"Logged in as {account.get('email')}" if isinstance(account, dict) and account.get("email") else ""
return {
"online": logged_in,
"model": self.model,
"base_url": command,
"provider": "codex",
"model_available": self.model in models if models else bool(self.model),
"models": models,
"message": "Codex App Server is online." if logged_in else "Codex CLI is installed, but not logged in with ChatGPT.",
"detail": detail,
}
async def _cloud_health(self, provider: str) -> dict[str, Any]:
if not self.api_key:
return {
"online": False,
"model": self.model,
"base_url": self.base_url,
"provider": "openai",
"provider": provider,
"model_available": False,
"models": [],
"message": "OpenAI is selected, but no OpenAI API key is configured.",
"message": f"{self._provider_label()} is selected, but no API key is configured.",
"detail": "",
}
try:
@@ -532,10 +648,10 @@ class OllamaAgent:
"online": False,
"model": self.model,
"base_url": self.base_url,
"provider": "openai",
"provider": provider,
"model_available": False,
"models": [],
"message": f"OpenAI is unreachable at {self.base_url} or rejected the API key.",
"message": f"{self._provider_label()} is unreachable at {self.base_url} or rejected the API key.",
"detail": str(exc),
}
models = sorted(item.get("id") for item in body.get("data", []) if item.get("id"))
@@ -543,10 +659,10 @@ class OllamaAgent:
"online": True,
"model": self.model,
"base_url": self.base_url,
"provider": "openai",
"provider": provider,
"model_available": self.model in models,
"models": models,
"message": "OpenAI is online.",
"message": f"{self._provider_label()} is online.",
}
def _openai_headers(self) -> dict[str, str]:
@@ -595,8 +711,27 @@ class OllamaAgent:
normalized.append(entry)
return normalized
def _codex_tool_catalog(self) -> list[dict[str, Any]]:
tools: list[dict[str, Any]] = []
for schema in self.tools.schemas:
if schema.get("type") != "function":
continue
function = schema.get("function") or {}
tools.append(
{
"name": function.get("name", ""),
"description": function.get("description", ""),
"parameters": function.get("parameters") or {"type": "object", "properties": {}},
}
)
return tools
def _provider_label(self) -> str:
return "OpenAI model" if self.provider == "openai" else "local model"
if self.provider == "openai":
return "OpenAI model"
if self.provider == "codex":
return "Codex model"
return "local model"
@staticmethod
def _merge_openai_tool_call(target: dict[int, dict[str, Any]], delta: dict[str, Any]) -> None:
@@ -615,6 +750,431 @@ class OllamaAgent:
def _ordered_tool_calls(tool_calls: dict[int, dict[str, Any]]) -> list[dict[str, Any]]:
return [tool_calls[index] for index in sorted(tool_calls)]
async def _codex_cli_turn(
self,
query: str,
messages: list[dict[str, Any]],
previous_interaction: dict[str, Any] | None = None,
thread_id: str | None = DEFAULT_THREAD_ID,
) -> dict[str, Any]:
return await self._codex_app_server_turn(
query,
messages,
previous_interaction=previous_interaction,
thread_id=thread_id,
)
async def _codex_app_server_turn(
self,
query: str,
messages: list[dict[str, Any]],
previous_interaction: dict[str, Any] | None = None,
thread_id: str | None = DEFAULT_THREAD_ID,
) -> dict[str, Any]:
prompt = self._codex_cli_prompt(
query,
messages,
previous_interaction=previous_interaction,
thread_id=thread_id,
)
final_text = ""
process = await self._start_codex_app_server()
request_id = 1
async def send_request(method: str, params: dict[str, Any] | None = None, timeout: int = 120) -> dict[str, Any]:
nonlocal request_id
current_id = request_id
request_id += 1
payload: dict[str, Any] = {"jsonrpc": "2.0", "id": current_id, "method": method}
if params is not None:
payload["params"] = params
await self._codex_app_server_write(process, payload)
while True:
message = await self._codex_app_server_read(process, timeout=timeout)
if message.get("id") == current_id:
if message.get("error"):
error = message["error"]
raise RuntimeError(error.get("message") or f"Codex App Server request failed: {error}")
return message.get("result") or {}
await self._handle_codex_app_server_message(process, message)
try:
await send_request(
"initialize",
{
"clientInfo": {"name": "TraderAI", "version": __version__},
"capabilities": {"experimentalApi": True},
},
timeout=30,
)
await self._codex_app_server_write(process, {"jsonrpc": "2.0", "method": "initialized", "params": {}})
thread = await send_request(
"thread/start",
{
"model": self.model,
"modelProvider": None,
"cwd": str(Path.cwd()),
"approvalPolicy": "never",
"sandbox": "read-only",
"baseInstructions": "You are TraderAI running through the local Codex App Server using ChatGPT OAuth.",
"developerInstructions": (
"Do not run shell commands, inspect files, or modify the workspace. "
"Answer only with JSON matching the requested output schema."
),
"ephemeral": True,
"experimentalRawEvents": False,
"persistExtendedHistory": False,
},
timeout=30,
)
thread_id_value = ((thread.get("thread") or {}).get("id") or thread.get("threadId") or "").strip()
if not thread_id_value:
raise RuntimeError(f"Codex App Server did not return a thread id: {thread!r}")
turn = await send_request(
"turn/start",
{
"threadId": thread_id_value,
"input": [{"type": "text", "text": prompt, "text_elements": []}],
"cwd": str(Path.cwd()),
"approvalPolicy": "never",
"sandboxPolicy": {"type": "readOnly", "access": {"type": "fullAccess"}},
"model": self.model,
"effort": self.reasoning_effort,
"summary": "none",
"outputSchema": self._codex_output_schema(),
},
timeout=60,
)
turn_id = ((turn.get("turn") or {}).get("id") or "").strip()
if not turn_id:
raise RuntimeError(f"Codex App Server did not return a turn id: {turn!r}")
while True:
message = await self._codex_app_server_read(process, timeout=240)
method = message.get("method")
params = message.get("params") or {}
if method == "item/agentMessage/delta" and params.get("turnId") == turn_id:
final_text += params.get("delta") or ""
elif method == "item/completed" and params.get("turnId") == turn_id:
item = params.get("item") or {}
if item.get("type") == "agentMessage":
final_text = item.get("text") or final_text
elif method == "turn/completed" and (params.get("turn") or {}).get("id") == turn_id:
turn_status = (params.get("turn") or {}).get("status")
if turn_status != "completed":
error = (params.get("turn") or {}).get("error") or {}
raise RuntimeError(error.get("message") or f"Codex App Server turn ended with status {turn_status}.")
break
elif method == "error":
error = params.get("message") or params.get("error") or params
raise RuntimeError(f"Codex App Server error: {error}")
else:
await self._handle_codex_app_server_message(process, message)
finally:
await self._stop_codex_app_server(process)
return self._parse_codex_app_server_text(final_text)
def _codex_cli_prompt(
self,
query: str,
messages: list[dict[str, Any]],
previous_interaction: dict[str, Any] | None = None,
thread_id: str | None = DEFAULT_THREAD_ID,
) -> str:
conversation_lines: list[str] = []
for message in self._messages_with_context(
query,
messages,
previous_interaction=previous_interaction,
thread_id=thread_id,
):
role = message.get("role", "unknown")
content = message.get("content", "")
suffix = ""
if role == "user" and message.get("images"):
suffix = f" [attached images: {len(message.get('images') or [])}]"
if role == "tool":
suffix = f" [tool {message.get('tool_name') or ''}]"
if role == "assistant" and message.get("tool_calls"):
suffix = f" [tool calls: {json.dumps(message.get('tool_calls'), ensure_ascii=True)}]"
conversation_lines.append(f"{role}{suffix}: {content}")
tools_json = json.dumps(self._codex_tool_catalog(), ensure_ascii=True, indent=2)
return (
"You are TraderAI running through the local Codex App Server using ChatGPT OAuth.\n"
"Do not run shell commands, inspect files, or modify the workspace.\n"
"Your only job is to decide whether to answer directly or request exactly one TraderAI tool.\n\n"
"Return JSON that matches the provided schema.\n"
"- If you can answer now, set kind to final, put the user-facing reply in message, set tool_name to an empty string, and set arguments_json to '{}'.\n"
"- If you need a tool, set kind to tool_call, set tool_name to the exact tool name, set message to an empty string, and set arguments_json to a valid JSON object string.\n"
"- Never return more than one tool call at a time.\n"
"- Prefer the TraderAI tools over guessing.\n\n"
f"Available tools:\n{tools_json}\n\n"
"Conversation transcript:\n"
+ "\n".join(conversation_lines)
)
def _codex_structured_response(self, result: dict[str, Any]) -> dict[str, Any]:
if result.get("kind") == "tool_call":
tool_name = str(result.get("tool_name") or "").strip()
arguments_json = str(result.get("arguments_json") or "{}").strip() or "{}"
return {
"message": {
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": f"codex-{uuid.uuid4()}",
"type": "function",
"function": {
"name": tool_name,
"arguments": arguments_json,
},
}
],
}
}
return {
"message": {
"role": "assistant",
"content": str(result.get("message") or ""),
"tool_calls": [],
}
}
def _write_codex_schema(self) -> str:
schema = self._codex_output_schema()
with tempfile.NamedTemporaryFile("w", suffix="-traderai-codex-schema.json", delete=False, encoding="utf-8") as handle:
json.dump(schema, handle, ensure_ascii=True)
return handle.name
@staticmethod
def _codex_output_schema() -> dict[str, Any]:
return {
"type": "object",
"properties": {
"kind": {"type": "string", "enum": ["final", "tool_call"]},
"message": {"type": "string"},
"tool_name": {"type": "string"},
"arguments_json": {"type": "string"},
},
"required": ["kind", "message", "tool_name", "arguments_json"],
"additionalProperties": False,
}
def _parse_codex_app_server_text(self, final_text: str) -> dict[str, Any]:
if not final_text.strip():
raise RuntimeError("Codex App Server returned an empty response.")
try:
parsed = json.loads(final_text)
except ValueError as exc:
raise RuntimeError(f"Codex App Server returned non-JSON output: {final_text}") from exc
if parsed.get("kind") not in {"final", "tool_call"}:
raise RuntimeError(f"Codex App Server returned an invalid result kind: {parsed!r}")
return parsed
def _parse_codex_exec_output(self, output: dict[str, Any]) -> dict[str, Any]:
events = output.get("events") or []
final_text = ""
error_text = ""
for event in events:
if event.get("type") == "item.completed":
item = event.get("item") or {}
if item.get("type") == "agent_message":
final_text = item.get("text") or final_text
elif event.get("type") == "error":
error_text = event.get("message") or error_text
elif event.get("type") == "turn.failed":
details = event.get("error") or {}
error_text = details.get("message") or error_text
if output.get("returncode") != 0 and not final_text:
raise RuntimeError(error_text or output.get("stderr") or "Codex CLI failed.")
try:
parsed = json.loads(final_text)
except ValueError as exc:
raise RuntimeError(f"Codex CLI returned non-JSON output: {final_text}") from exc
if parsed.get("kind") not in {"final", "tool_call"}:
raise RuntimeError(f"Codex CLI returned an invalid result kind: {parsed!r}")
return parsed
def _codex_command(self, required: bool = False) -> str | None:
configured = self.base_url.strip() if self.base_url else "codex"
resolved = shutil.which(configured) or configured
if required and not Path(resolved).exists() and shutil.which(resolved) is None:
raise RuntimeError("Codex CLI was not found on PATH.")
return resolved
async def _codex_app_server_status(self) -> tuple[dict[str, Any] | None, list[str]]:
process = await self._start_codex_app_server()
request_id = 1
async def send_request(method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
nonlocal request_id
current_id = request_id
request_id += 1
payload: dict[str, Any] = {"jsonrpc": "2.0", "id": current_id, "method": method}
if params is not None:
payload["params"] = params
await self._codex_app_server_write(process, payload)
while True:
message = await self._codex_app_server_read(process, timeout=30)
if message.get("id") == current_id:
if message.get("error"):
error = message["error"]
raise RuntimeError(error.get("message") or f"Codex App Server request failed: {error}")
return message.get("result") or {}
await self._handle_codex_app_server_message(process, message)
try:
await send_request(
"initialize",
{
"clientInfo": {"name": "TraderAI", "version": __version__},
"capabilities": {"experimentalApi": True},
},
)
await self._codex_app_server_write(process, {"jsonrpc": "2.0", "method": "initialized", "params": {}})
account_result = await send_request("account/read", {"refreshToken": False})
models: list[str] = []
cursor: str | None = None
for _ in range(20):
params: dict[str, Any] = {"limit": 50, "includeHidden": False}
if cursor:
params["cursor"] = cursor
page = await send_request("model/list", params)
for item in page.get("data") or []:
model = item.get("id") or item.get("model")
if model:
models.append(model)
cursor = page.get("nextCursor")
if not cursor:
break
return account_result.get("account"), sorted(set(models))
finally:
await self._stop_codex_app_server(process)
async def _start_codex_app_server(self) -> asyncio.subprocess.Process:
return await asyncio.create_subprocess_exec(
self._codex_command(required=True),
"app-server",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
async def _codex_app_server_write(self, process: asyncio.subprocess.Process, payload: dict[str, Any]) -> None:
if process.stdin is None:
raise RuntimeError("Codex App Server stdin is unavailable.")
process.stdin.write((json.dumps(payload, ensure_ascii=True) + "\n").encode("utf-8"))
await process.stdin.drain()
async def _codex_app_server_read(self, process: asyncio.subprocess.Process, timeout: int) -> dict[str, Any]:
if process.stdout is None:
raise RuntimeError("Codex App Server stdout is unavailable.")
try:
line = await asyncio.wait_for(process.stdout.readline(), timeout=timeout)
except TimeoutError as exc:
raise RuntimeError("Codex App Server timed out.") from exc
if not line:
stderr = ""
if process.stderr is not None:
try:
stderr = (await asyncio.wait_for(process.stderr.read(), timeout=1)).decode("utf-8", errors="replace").strip()
except TimeoutError:
stderr = ""
raise RuntimeError(stderr or "Codex App Server exited without a response.")
try:
return json.loads(line.decode("utf-8", errors="replace"))
except ValueError as exc:
raise RuntimeError(f"Codex App Server returned invalid JSON-RPC: {line!r}") from exc
async def _handle_codex_app_server_message(self, process: asyncio.subprocess.Process, message: dict[str, Any]) -> None:
if "id" not in message or "method" not in message:
return
method = message.get("method")
if method in {
"item/commandExecution/requestApproval",
"item/fileChange/requestApproval",
"applyPatchApproval",
"execCommandApproval",
}:
await self._codex_app_server_write(
process,
{
"jsonrpc": "2.0",
"id": message["id"],
"result": {
"decision": "deny",
"message": "TraderAI does not allow Codex to run commands or change files.",
},
},
)
return
await self._codex_app_server_write(
process,
{
"jsonrpc": "2.0",
"id": message["id"],
"error": {"code": -32601, "message": f"TraderAI does not handle Codex App Server request {method}."},
},
)
async def _stop_codex_app_server(self, process: asyncio.subprocess.Process) -> None:
if process.returncode is not None:
return
process.terminate()
try:
await asyncio.wait_for(process.wait(), timeout=3)
except TimeoutError:
process.kill()
await process.wait()
async def _run_command(self, command: list[str], timeout: int = 120, stdin_text: str | None = None) -> dict[str, Any]:
process = await asyncio.create_subprocess_exec(
*command,
stdin=asyncio.subprocess.PIPE if stdin_text is not None else None,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
try:
payload = stdin_text.encode("utf-8") if stdin_text is not None else None
stdout, stderr = await asyncio.wait_for(process.communicate(payload), timeout=timeout)
except TimeoutError:
process.kill()
await process.communicate()
raise RuntimeError(f"Command timed out: {' '.join(command[:3])}")
stdout_text = stdout.decode("utf-8", errors="replace")
stderr_text = stderr.decode("utf-8", errors="replace")
events = []
for line in stdout_text.splitlines():
line = line.strip()
if not line:
continue
try:
events.append(json.loads(line))
except ValueError:
events.append({"type": "stdout", "text": line})
return {
"returncode": process.returncode,
"stdout": stdout_text,
"stderr": stderr_text,
"events": events,
}
def _codex_model_cache(self) -> list[str]:
cache_path = Path.home() / ".codex" / "models_cache.json"
if not cache_path.exists():
return []
try:
body = json.loads(cache_path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return []
models = []
for item in body.get("models", []):
slug = item.get("slug")
if slug:
models.append(slug)
return sorted(set(models))
def _runtime_context(
self,
query: str,
@@ -741,6 +1301,16 @@ class OllamaAgent:
choice = (response.json().get("choices") or [{}])[0]
message = choice.get("message") or {}
return self._clean_generated_title(message.get("content", ""))
if self.provider == "codex":
result = await self._codex_app_server_turn(
prompt,
[
{"role": "system", "content": "You write short chat titles."},
{"role": "user", "content": prompt},
],
thread_id="title",
)
return self._clean_generated_title(result.get("message", ""))
async with httpx.AsyncClient(timeout=20) as client:
response = await client.post(
f"{self.base_url}/api/chat",
@@ -831,6 +1401,10 @@ class OllamaAgent:
"list_scmdb_versions": "Checking SCMDB versions",
"search_scmdb_missions": "Searching SCMDB missions",
"get_scmdb_mission_rewards": "Fetching SCMDB mission rewards",
"search_scwiki_pages": "Searching Star Citizen Wiki",
"get_scwiki_page": "Reading Star Citizen Wiki page",
"search_scwiki_vehicles": "Searching Star Citizen Wiki vehicles",
"get_scwiki_vehicle": "Fetching Star Citizen Wiki vehicle",
"search_cornerstone_items": "Searching Cornerstone items",
"get_cornerstone_item_locations": "Fetching Cornerstone item locations",
"get_cornerstone_item_media": "Fetching Cornerstone item media",