major agent refactoring: wiki knowledge base, no RAG, no Qdrant, no Ollama

This commit is contained in:
2026-04-21 21:03:24 -04:00
parent 7e4b54d701
commit 44a1688657
80 changed files with 2699 additions and 4267 deletions

View File

@@ -158,7 +158,7 @@ def _remove_indicator_instances(workspace_store, pandas_ta_name: str) -> None:
def _workspace_sync_content(workspace_store, category: str) -> "TextContent | None":
"""
Return a TextContent item carrying the current {category}_types workspace state so the
gateway can sync it to connected web clients without a separate workspace_patch call.
gateway can sync it to connected web clients without a separate WorkspacePatch call.
The gateway detects items of the form {"_workspace_sync": {"store": ..., "data": ...}}.
"""
store = _type_store_name(category)
@@ -304,7 +304,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
@server.list_resources()
async def list_resources():
"""List available resources"""
return [
resources = [
{
"uri": f"dexorder://user/{config.user_id}/hello",
"name": "Hello World",
@@ -312,6 +312,14 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
"mimeType": "text/plain",
}
]
if _get_env_yml() is not None:
resources.append({
"uri": f"dexorder://user/{config.user_id}/environment.yml",
"name": "Conda Environment",
"description": "Base conda environment packages available in all scripts",
"mimeType": "text/yaml",
})
return resources
@server.read_resource()
async def read_resource(uri: str):
@@ -332,6 +340,15 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
"mimeType": "text/plain",
"text": f"Hello from Dexorder user container!\nUser ID: {config.user_id}\n",
}
elif uri == f"dexorder://user/{config.user_id}/environment.yml":
env_yml = _get_env_yml()
if env_yml is None:
raise ValueError("environment.yml not found")
return {
"uri": uri,
"mimeType": "text/yaml",
"text": env_yml.read_text(encoding="utf-8"),
}
else:
raise ValueError(f"Unknown resource: {uri}")
@@ -340,7 +357,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
"""List available tools including workspace and category tools"""
return [
Tool(
name="workspace_read",
name="WorkspaceRead",
description="Read a workspace store from persistent storage",
inputSchema={
"type": "object",
@@ -354,7 +371,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="workspace_write",
name="WorkspaceWrite",
description="Write a workspace store to persistent storage",
inputSchema={
"type": "object",
@@ -371,7 +388,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="workspace_patch",
name="WorkspacePatch",
description="Apply JSON patch operations to a workspace store",
inputSchema={
"type": "object",
@@ -398,7 +415,47 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="python_write",
name="PreferencesRead",
description="Read the user preferences markdown file. Returns the full content of preferences.md from the user's sandbox data directory.",
inputSchema={
"type": "object",
"properties": {}
}
),
Tool(
name="PreferencesWrite",
description="Write (fully replace) the user preferences markdown file. Use this to create or overwrite preferences.md with new content.",
inputSchema={
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "Full markdown content for the preferences file"
}
},
"required": ["content"]
}
),
Tool(
name="PreferencesPatch",
description="Surgically update a section of the user preferences markdown file by finding and replacing text. Fails if old_str is not found.",
inputSchema={
"type": "object",
"properties": {
"old_str": {
"type": "string",
"description": "Exact text to find in the preferences file"
},
"new_str": {
"type": "string",
"description": "Replacement text"
}
},
"required": ["old_str", "new_str"]
}
),
Tool(
name="PythonWrite",
description="Write a new strategy, indicator, or research script with validation",
inputSchema={
"type": "object",
@@ -422,7 +479,8 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
"Full markdown description of the code with sufficient detail that another coding agent "
"could functionally reproduce the implementation from this field alone. "
"Include: purpose, algorithm, all parameters and their semantics, data feed usage, "
"formulas, edge cases, and any non-obvious implementation choices (required)."
"formulas, edge cases, and any non-obvious implementation choices (required). "
"Stored as a separate details.md file alongside the implementation."
)
},
"code": {
@@ -446,7 +504,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="python_edit",
name="PythonEdit",
description=(
"Edit an existing category script. "
"Use 'patches' for targeted string replacements (preferred for small changes), "
@@ -525,8 +583,8 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="python_read",
description="Read a category script and its metadata",
name="PythonRead",
description="Read a category script, its metadata, and details. Returns: code, details (markdown), and metadata.",
inputSchema={
"type": "object",
"properties": {
@@ -544,7 +602,38 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="python_list",
name="PythonReadOutput",
description=(
"Read persisted output files from a previous research script execution. "
"Returns TextContent for .md/.txt files and ImageContent for images. "
"Output is saved automatically when ExecuteResearch or PythonWrite/PythonEdit runs a research script."
),
inputSchema={
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["strategy", "indicator", "research"],
"description": "Category of the script"
},
"name": {
"type": "string",
"description": "Display name of the item"
},
"files": {
"type": "array",
"items": {"type": "string"},
"description": (
"Specific filenames under output/ to return (e.g. [\"analysis.md\", \"img1.png\"]). "
"If omitted, returns all output files listed in metadata."
)
}
},
"required": ["category", "name"]
}
),
Tool(
name="PythonList",
description="List all items in a category with names and descriptions",
inputSchema={
"type": "object",
@@ -559,7 +648,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="python_log",
name="PythonLog",
description="Show git commit history for category items. Filter by category and/or name to see history for a specific item.",
inputSchema={
"type": "object",
@@ -583,7 +672,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="python_revert",
name="PythonRevert",
description="Restore a category item to a previous git revision. Creates a new commit — non-destructive.",
inputSchema={
"type": "object",
@@ -606,7 +695,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="python_delete",
name="PythonDelete",
description="Delete a category script permanently. Commits removal to git history and removes any conda packages that are no longer needed.",
inputSchema={
"type": "object",
@@ -625,7 +714,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="conda_sync",
name="CondaSync",
description="Sync conda packages: scan all metadata, remove unused packages (excluding base environment)",
inputSchema={
"type": "object",
@@ -634,7 +723,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="conda_install",
name="CondaInstall",
description="Install conda packages on-demand",
inputSchema={
"type": "object",
@@ -649,7 +738,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="execute_research",
name="ExecuteResearch",
description="Execute a research script and return results with matplotlib images",
inputSchema={
"type": "object",
@@ -663,7 +752,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="evaluate_indicator",
name="EvaluateIndicator",
description=(
"Evaluate a pandas-ta indicator against real OHLC data and return a structured "
"array of timestamped values. Use this to validate that an indicator computes "
@@ -701,7 +790,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="backtest_strategy",
name="BacktestStrategy",
description=(
"Run a saved trading strategy against historical OHLC data using Nautilus Trader "
"BacktestEngine. Returns performance metrics (total return, Sharpe ratio, "
@@ -714,7 +803,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
"properties": {
"strategy_name": {
"type": "string",
"description": "Display name of the strategy as saved via python_write"
"description": "Display name of the strategy as saved via PythonWrite"
},
"feeds": {
"type": "array",
@@ -757,7 +846,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="activate_strategy",
name="ActivateStrategy",
description=(
"Activate a strategy for paper or live forward trading with a capital allocation. "
"paper=true (default): simulated fills on live data — no API keys required. "
@@ -769,7 +858,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
"properties": {
"strategy_name": {
"type": "string",
"description": "Display name of the strategy as saved via python_write"
"description": "Display name of the strategy as saved via PythonWrite"
},
"feeds": {
"type": "array",
@@ -798,7 +887,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="deactivate_strategy",
name="DeactivateStrategy",
description="Stop an active strategy and return its final P&L summary.",
inputSchema={
"type": "object",
@@ -812,7 +901,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="list_active_strategies",
name="ListActiveStrategies",
description="List all currently active (live or paper) strategies and their status.",
inputSchema={
"type": "object",
@@ -821,7 +910,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="get_backtest_results",
name="GetBacktestResults",
description=(
"Retrieve stored backtest results for a strategy. "
"Returns the most recent backtest runs with summary stats, "
@@ -844,7 +933,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="get_strategy_trades",
name="GetStrategyTrades",
description=(
"Retrieve the trade log for a strategy (live/paper or backtest). "
"Returns individual round-trip trades with entry/exit prices and PnL."
@@ -866,7 +955,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
}
),
Tool(
name="get_strategy_events",
name="GetStrategyEvents",
description=(
"Retrieve the event log for a strategy "
"(PnL updates, fills, errors, status changes)."
@@ -905,19 +994,38 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
raise
async def _handle_tool_call_inner(name: str, arguments: dict):
if name == "workspace_read":
if name == "WorkspaceRead":
return workspace_store.read(arguments.get("store_name", ""))
elif name == "workspace_write":
elif name == "WorkspaceWrite":
return workspace_store.write(
arguments.get("store_name", ""),
arguments.get("data")
)
elif name == "workspace_patch":
elif name == "WorkspacePatch":
return workspace_store.patch(
arguments.get("store_name", ""),
arguments.get("patch", [])
)
elif name == "python_write":
elif name == "PreferencesRead":
prefs_path = DATA_DIR / "preferences.md"
if not prefs_path.exists():
return {"content": "", "exists": False}
content = prefs_path.read_text(encoding="utf-8")
return {"content": content, "exists": True}
elif name == "PreferencesWrite":
prefs_path = DATA_DIR / "preferences.md"
prefs_path.write_text(arguments.get("content", ""), encoding="utf-8")
return {"success": True}
elif name == "PreferencesPatch":
prefs_path = DATA_DIR / "preferences.md"
old_str = arguments.get("old_str", "")
new_str = arguments.get("new_str", "")
content = prefs_path.read_text(encoding="utf-8") if prefs_path.exists() else ""
if old_str not in content:
return {"success": False, "error": "old_str not found in preferences file"}
prefs_path.write_text(content.replace(old_str, new_str, 1), encoding="utf-8")
return {"success": True}
elif name == "PythonWrite":
result = await category_manager.write(
category=arguments.get("category", ""),
name=arguments.get("name", ""),
@@ -941,9 +1049,9 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
exec_content = result["execution"].get("content", [])
content.extend(exec_content)
image_count = sum(1 for item in exec_content if item.type == "image")
logging.info(f"python_write '{arguments.get('name')}': returning {len(content)} items, {image_count} images")
logging.info(f"PythonWrite '{arguments.get('name')}': returning {len(content)} items, {image_count} images")
else:
logging.info(f"python_write '{arguments.get('name')}': no execution result (category={arguments.get('category')})")
logging.info(f"PythonWrite '{arguments.get('name')}': no execution result (category={arguments.get('category')})")
if result.get("success"):
_upsert_type(workspace_store, category_manager, arguments.get("category", ""), arguments.get("name", ""))
await cleanup_extra_packages_async(get_data_dir(), _get_env_yml())
@@ -951,7 +1059,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
if sync:
content.append(sync)
return content
elif name == "python_edit":
elif name == "PythonEdit":
result = await category_manager.edit(
category=arguments.get("category", ""),
name=arguments.get("name", ""),
@@ -977,9 +1085,9 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
exec_content = result["execution"].get("content", [])
content.extend(exec_content)
image_count = sum(1 for item in exec_content if item.type == "image")
logging.info(f"python_edit '{arguments.get('name')}': returning {len(content)} items, {image_count} images")
logging.info(f"PythonEdit '{arguments.get('name')}': returning {len(content)} items, {image_count} images")
else:
logging.info(f"python_edit '{arguments.get('name')}': no execution result")
logging.info(f"PythonEdit '{arguments.get('name')}': no execution result")
if result.get("success"):
_upsert_type(workspace_store, category_manager, arguments.get("category", ""), arguments.get("name", ""))
await cleanup_extra_packages_async(get_data_dir(), _get_env_yml())
@@ -987,16 +1095,30 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
if sync:
content.append(sync)
return content
elif name == "python_read":
elif name == "PythonRead":
return category_manager.read(
category=arguments.get("category", ""),
name=arguments.get("name", "")
)
elif name == "python_list":
elif name == "PythonReadOutput":
result = category_manager.read_output(
category=arguments.get("category", ""),
name=arguments.get("name", ""),
files=arguments.get("files"),
)
if "error" in result:
return [TextContent(type="text", text=f"Error: {result['error']}")]
content = result.get("content", [])
summary = TextContent(
type="text",
text=f"output_dir: {result.get('output_dir', '')}\nfiles_returned: {result.get('files_returned', [])}"
)
return [summary] + content
elif name == "PythonList":
return category_manager.list_items(
category=arguments.get("category", "")
)
elif name == "python_log":
elif name == "PythonLog":
result = await category_manager.git_log(
category=arguments.get("category"),
name=arguments.get("name"),
@@ -1006,7 +1128,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
for c in result.get("commits", []):
lines.append(f"{c['short_hash']} {c['date'][:10]} {c['message']}")
return [TextContent(type="text", text="\n".join(lines))]
elif name == "python_revert":
elif name == "PythonRevert":
result = await category_manager.git_revert(
revision=arguments.get("revision", ""),
category=arguments.get("category", ""),
@@ -1027,7 +1149,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
content_out.append(sync)
return content_out
return [TextContent(type="text", text="\n".join(meta_parts))]
elif name == "python_delete":
elif name == "PythonDelete":
result = await category_manager.delete(
category=arguments.get("category", ""),
name=arguments.get("name", "")
@@ -1047,23 +1169,23 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
if sync:
content_out.append(sync)
return content_out
elif name == "conda_sync":
elif name == "CondaSync":
return await sync_packages_async(
data_dir=get_data_dir(),
environment_yml=_get_env_yml()
)
elif name == "conda_install":
elif name == "CondaInstall":
return await install_packages_async(arguments.get("packages", []))
elif name == "execute_research":
elif name == "ExecuteResearch":
result = await category_manager.execute_research(name=arguments.get("name", ""))
if "error" in result:
logging.error(f"execute_research '{arguments.get('name')}': {result['error']}")
logging.error(f"ExecuteResearch '{arguments.get('name')}': {result['error']}")
return [TextContent(type="text", text=f"Error: {result['error']}")]
content = result.get("content", [TextContent(type="text", text="No output")])
image_count = sum(1 for item in content if item.type == "image")
logging.info(f"execute_research '{arguments.get('name')}': returning {len(content)} items, {image_count} images")
logging.info(f"ExecuteResearch '{arguments.get('name')}': returning {len(content)} items, {image_count} images")
return content
elif name == "evaluate_indicator":
elif name == "EvaluateIndicator":
return await evaluate_indicator(
symbol=arguments.get("symbol", ""),
from_time=arguments.get("from_time"),
@@ -1072,7 +1194,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
pandas_ta_name=arguments.get("pandas_ta_name", ""),
parameters=arguments.get("parameters") or {},
)
elif name == "backtest_strategy":
elif name == "BacktestStrategy":
result = await backtest_strategy(
strategy_name=arguments.get("strategy_name", ""),
feeds=arguments.get("feeds", []),
@@ -1101,20 +1223,20 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
except Exception as _e:
logging.debug("Failed to persist backtest results: %s", _e)
return result
elif name == "activate_strategy":
elif name == "ActivateStrategy":
return await activate_strategy(
strategy_name=arguments.get("strategy_name", ""),
feeds=arguments.get("feeds", []),
allocation=float(arguments.get("allocation", 0.0)),
paper=bool(arguments.get("paper", True)),
)
elif name == "deactivate_strategy":
elif name == "DeactivateStrategy":
return await deactivate_strategy(
strategy_name=arguments.get("strategy_name", ""),
)
elif name == "list_active_strategies":
elif name == "ListActiveStrategies":
return await list_active_strategies()
elif name == "get_backtest_results":
elif name == "GetBacktestResults":
from dexorder.strategy.db import get_strategy_db
db = get_strategy_db(get_data_dir())
results = await db.get_backtests(
@@ -1122,7 +1244,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
limit=int(arguments.get("limit", 5)),
)
return [TextContent(type="text", text=json.dumps({"backtest_runs": results}))]
elif name == "get_strategy_trades":
elif name == "GetStrategyTrades":
from dexorder.strategy.db import get_strategy_db
db = get_strategy_db(get_data_dir())
trades = await db.get_trades(
@@ -1130,7 +1252,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
limit=int(arguments.get("limit", 100)),
)
return [TextContent(type="text", text=json.dumps({"trades": trades}))]
elif name == "get_strategy_events":
elif name == "GetStrategyEvents":
from dexorder.strategy.db import get_strategy_db
db = get_strategy_db(get_data_dir())
events = await db.get_events(