From feaab45032428227144a0b80ce68660ed5b2e156 Mon Sep 17 00:00:00 2001 From: silkspace Date: Wed, 18 Mar 2026 12:52:23 -0700 Subject: [PATCH 1/3] feat: mock viz MCP server for Talk2Graph integration testing Simulates the MCP server that runs inside streamgl-viz workers (PR #3043). Provides 7 tools matching the Talk2Graph surface with two mock datasets (cybersecurity network traffic and social media influence graph). --- src/graphistry_mcp_server/mock_viz_server.py | 319 +++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 src/graphistry_mcp_server/mock_viz_server.py diff --git a/src/graphistry_mcp_server/mock_viz_server.py b/src/graphistry_mcp_server/mock_viz_server.py new file mode 100644 index 0000000..4773897 --- /dev/null +++ b/src/graphistry_mcp_server/mock_viz_server.py @@ -0,0 +1,319 @@ +""" +Mock Graphistry Viz MCP Server for Talk2Graph development. + +Simulates the MCP server that runs inside streamgl-viz workers (PR #3043). +Use this to test Louie's Talk2Graph integration without a running Graphistry instance. + +Run standalone: + uv run python -m graphistry_mcp_server.mock_viz_server + +Register as MCP connector on Louie (env var): + MCP_SERVER_URL=http://localhost:3100/mcp + +Or register via CreateConnector API event: + {"type": "CreateConnector", "connector": { + "type": "MCP", "name": "Talk2Graph (mock)", + "config": {"server_url": "http://localhost:3100/mcp"} + }} +""" + +import random +from typing import Any, Dict, List, Optional + +from mcp.server.fastmcp import FastMCP + +mock_mcp = FastMCP("graphistry-viz-mock") + +# --- Mock session data (cybersecurity dataset) --- + +MOCK_SESSIONS: Dict[str, Dict[str, Any]] = { + "mock-session-001": { + "session_id": "mock-session-001", + "numNodes": 1247, + "numEdges": 3891, + "title": "Enterprise Network Traffic", + "description": "Zeek conn.log data from 2026-03-15, 6-hour capture window", + "nodeColumns": [ + "ip_address", "hostname", "country", "asn", "threat_score", + "is_malicious", "device_type", "department", "first_seen", "last_seen", + ], + "edgeColumns": [ + "src_ip", "dst_ip", "protocol", "service", "bytes_sent", "bytes_recv", + "duration", "timestamp", "conn_state", "is_encrypted", + ], + "active_encodings": {}, + "active_filters": [], + "node_sample": [ + {"ip_address": "10.0.1.15", "hostname": "ws-jdoe", "country": "US", "asn": "AS64496", "threat_score": 0.12, "is_malicious": False, "device_type": "workstation", "department": "Engineering", "first_seen": "2026-03-15T08:00:12Z", "last_seen": "2026-03-15T14:00:00Z"}, + {"ip_address": "10.0.2.42", "hostname": "srv-db-01", "country": "US", "asn": "AS64496", "threat_score": 0.05, "is_malicious": False, "device_type": "server", "department": "IT", "first_seen": "2026-03-15T00:00:00Z", "last_seen": "2026-03-15T14:00:00Z"}, + {"ip_address": "185.220.101.34", "hostname": None, "country": "DE", "asn": "AS24940", "threat_score": 0.91, "is_malicious": True, "device_type": "unknown", "department": None, "first_seen": "2026-03-15T09:14:33Z", "last_seen": "2026-03-15T13:45:10Z"}, + {"ip_address": "10.0.3.8", "hostname": "ws-asmith", "country": "US", "asn": "AS64496", "threat_score": 0.67, "is_malicious": False, "device_type": "workstation", "department": "Finance", "first_seen": "2026-03-15T08:30:00Z", "last_seen": "2026-03-15T13:59:00Z"}, + {"ip_address": "91.219.237.12", "hostname": None, "country": "RU", "asn": "AS57678", "threat_score": 0.88, "is_malicious": True, "device_type": "unknown", "department": None, "first_seen": "2026-03-15T10:22:17Z", "last_seen": "2026-03-15T12:10:45Z"}, + ], + "edge_sample": [ + {"src_ip": "10.0.1.15", "dst_ip": "10.0.2.42", "protocol": "TCP", "service": "postgresql", "bytes_sent": 2048, "bytes_recv": 65536, "duration": 12.4, "timestamp": "2026-03-15T08:01:00Z", "conn_state": "SF", "is_encrypted": True}, + {"src_ip": "185.220.101.34", "dst_ip": "10.0.1.15", "protocol": "TCP", "service": "ssh", "bytes_sent": 4096, "bytes_recv": 512, "duration": 0.3, "timestamp": "2026-03-15T09:14:33Z", "conn_state": "REJ", "is_encrypted": False}, + {"src_ip": "10.0.3.8", "dst_ip": "91.219.237.12", "protocol": "TCP", "service": "http", "bytes_sent": 128, "bytes_recv": 32768, "duration": 2.1, "timestamp": "2026-03-15T10:30:00Z", "conn_state": "SF", "is_encrypted": False}, + {"src_ip": "10.0.1.15", "dst_ip": "10.0.3.8", "protocol": "UDP", "service": "dns", "bytes_sent": 64, "bytes_recv": 256, "duration": 0.01, "timestamp": "2026-03-15T08:00:15Z", "conn_state": "SF", "is_encrypted": False}, + {"src_ip": "10.0.3.8", "dst_ip": "185.220.101.34", "protocol": "TCP", "service": "https", "bytes_sent": 512, "bytes_recv": 16384, "duration": 5.7, "timestamp": "2026-03-15T11:00:00Z", "conn_state": "SF", "is_encrypted": True}, + ], + }, + "mock-session-002": { + "session_id": "mock-session-002", + "numNodes": 342, + "numEdges": 1205, + "title": "Social Media Influence Network", + "description": "Twitter/X interaction graph from #CyberSec community, March 2026", + "nodeColumns": [ + "handle", "display_name", "followers", "following", "verified", + "account_age_days", "avg_engagement", "bot_score", "community_id", "location", + ], + "edgeColumns": [ + "source_handle", "target_handle", "interaction_type", "count", + "sentiment", "timestamp_first", "timestamp_last", + ], + "active_encodings": {}, + "active_filters": [], + "node_sample": [ + {"handle": "@threat_intel_lab", "display_name": "Threat Intel Lab", "followers": 45200, "following": 312, "verified": True, "account_age_days": 2190, "avg_engagement": 3.2, "bot_score": 0.02, "community_id": 1, "location": "San Francisco, CA"}, + {"handle": "@sec_researcher_99", "display_name": "SecRes99", "followers": 1200, "following": 890, "verified": False, "account_age_days": 365, "avg_engagement": 0.8, "bot_score": 0.15, "community_id": 2, "location": "London, UK"}, + {"handle": "@bot_farm_x42", "display_name": "News Updates Daily", "followers": 50, "following": 5000, "verified": False, "account_age_days": 30, "avg_engagement": 0.01, "bot_score": 0.95, "community_id": 3, "location": None}, + {"handle": "@ciso_weekly", "display_name": "CISO Weekly Digest", "followers": 28400, "following": 150, "verified": True, "account_age_days": 1825, "avg_engagement": 5.1, "bot_score": 0.01, "community_id": 1, "location": "New York, NY"}, + {"handle": "@apt_tracker", "display_name": "APT Tracker", "followers": 8900, "following": 420, "verified": False, "account_age_days": 730, "avg_engagement": 2.4, "bot_score": 0.08, "community_id": 2, "location": "Berlin, DE"}, + ], + "edge_sample": [ + {"source_handle": "@threat_intel_lab", "target_handle": "@ciso_weekly", "interaction_type": "retweet", "count": 47, "sentiment": 0.8, "timestamp_first": "2026-01-15T10:00:00Z", "timestamp_last": "2026-03-14T18:30:00Z"}, + {"source_handle": "@sec_researcher_99", "target_handle": "@threat_intel_lab", "interaction_type": "reply", "count": 12, "sentiment": 0.6, "timestamp_first": "2026-02-01T14:00:00Z", "timestamp_last": "2026-03-10T09:00:00Z"}, + {"source_handle": "@bot_farm_x42", "target_handle": "@apt_tracker", "interaction_type": "mention", "count": 200, "sentiment": 0.0, "timestamp_first": "2026-03-01T00:00:00Z", "timestamp_last": "2026-03-14T23:59:00Z"}, + {"source_handle": "@apt_tracker", "target_handle": "@sec_researcher_99", "interaction_type": "quote", "count": 5, "sentiment": 0.7, "timestamp_first": "2026-02-20T11:00:00Z", "timestamp_last": "2026-03-12T16:00:00Z"}, + {"source_handle": "@ciso_weekly", "target_handle": "@threat_intel_lab", "interaction_type": "retweet", "count": 31, "sentiment": 0.9, "timestamp_first": "2026-01-20T08:00:00Z", "timestamp_last": "2026-03-13T12:00:00Z"}, + ], + }, +} + + +def _get_session(session_id: str) -> Dict[str, Any]: + if session_id not in MOCK_SESSIONS: + raise ValueError( + f"Session '{session_id}' not found. " + f"Available: {list(MOCK_SESSIONS.keys())}" + ) + return MOCK_SESSIONS[session_id] + + +# --- MCP Tools (matching PR #3043 tool surface) --- + + +@mock_mcp.tool() +async def list_sessions() -> Dict[str, Any]: + """List all active Graphistry visualization sessions.""" + sessions = list(MOCK_SESSIONS.keys()) + return {"sessions": sessions, "count": len(sessions)} + + +@mock_mcp.tool() +async def get_session_info( + session_id: str, + verbosity: str = "brief", +) -> Dict[str, Any]: + """ + Get information about a Graphistry visualization session. + + Args: + session_id: The Graphistry session ID + verbosity: 'brief' for schema only, 'full' to include sample values + + Returns: + Session metadata including node/edge counts, column names, + active encodings, and active filters. + """ + s = _get_session(session_id) + result: Dict[str, Any] = { + "session_id": s["session_id"], + "numNodes": s["numNodes"], + "numEdges": s["numEdges"], + "title": s["title"], + "description": s["description"], + "nodeColumns": s["nodeColumns"], + "edgeColumns": s["edgeColumns"], + "active_encodings": s["active_encodings"], + "active_filters": s["active_filters"], + } + if verbosity == "full": + result["node_sample"] = s["node_sample"][:3] + result["edge_sample"] = s["edge_sample"][:3] + return result + + +@mock_mcp.tool() +async def set_encoding( + session_id: str, + graph_type: str, + encoding_type: str, + attribute: str, + animate: bool = False, +) -> Dict[str, Any]: + """ + Set a visual encoding (color or size) on nodes or edges based on a data column. + + Args: + session_id: The Graphistry session ID + graph_type: 'point' for nodes, 'edge' for edges + encoding_type: 'color' or 'size' + attribute: The data column name to encode by + animate: Whether to animate the transition (default False) + """ + s = _get_session(session_id) + + columns = s["nodeColumns"] if graph_type == "point" else s["edgeColumns"] + if attribute not in columns: + return { + "success": False, + "error": f"Column '{attribute}' not found in {graph_type} columns. " + f"Available: {columns}", + } + + key = f"{graph_type}_{encoding_type}" + s["active_encodings"][key] = {"attribute": attribute, "animate": animate} + + return { + "success": True, + "message": f"Applied {encoding_type} encoding on {graph_type} by '{attribute}'" + + (" (animated)" if animate else ""), + "active_encodings": s["active_encodings"], + } + + +@mock_mcp.tool() +async def reset_encoding( + session_id: str, + graph_type: str, + encoding_type: str, +) -> Dict[str, Any]: + """ + Reset a visual encoding (color or size) on nodes or edges back to the default. + + Args: + session_id: The Graphistry session ID + graph_type: 'point' for nodes, 'edge' for edges + encoding_type: 'color' or 'size' + """ + s = _get_session(session_id) + key = f"{graph_type}_{encoding_type}" + removed = s["active_encodings"].pop(key, None) + + return { + "success": True, + "message": f"Reset {encoding_type} encoding on {graph_type}" + + (f" (was: {removed['attribute']})" if removed else " (was already default)"), + "active_encodings": s["active_encodings"], + } + + +@mock_mcp.tool() +async def add_filter( + session_id: str, + attribute: str, + operator: str, + value: Any, +) -> Dict[str, Any]: + """ + Add a filter to the visualization to show only nodes/edges matching a condition. + + Args: + session_id: The Graphistry session ID + attribute: The column name to filter on + operator: Comparison operator (eq, neq, gt, gte, lt, lte, contains) + value: The value to compare against (string or number) + """ + s = _get_session(session_id) + + all_columns = s["nodeColumns"] + s["edgeColumns"] + if attribute not in all_columns: + return { + "success": False, + "error": f"Column '{attribute}' not found. " + f"Node columns: {s['nodeColumns']}. Edge columns: {s['edgeColumns']}", + } + + filter_entry = {"attribute": attribute, "operator": operator, "value": value} + s["active_filters"].append(filter_entry) + + # Simulate reduced counts + reduction = random.uniform(0.1, 0.5) + visible_nodes = int(s["numNodes"] * (1 - reduction)) + visible_edges = int(s["numEdges"] * (1 - reduction * 1.3)) + + op_symbols = {"eq": "==", "neq": "!=", "gt": ">", "gte": ">=", "lt": "<", "lte": "<=", "contains": "contains"} + op_str = op_symbols.get(operator, operator) + + return { + "success": True, + "message": f"Filter applied: {attribute} {op_str} {value}", + "visible_nodes": visible_nodes, + "visible_edges": max(0, visible_edges), + "total_filters": len(s["active_filters"]), + } + + +@mock_mcp.tool() +async def reset_filters(session_id: str) -> Dict[str, Any]: + """ + Clear all filters from the visualization, showing all nodes and edges. + + Args: + session_id: The Graphistry session ID + """ + s = _get_session(session_id) + count = len(s["active_filters"]) + s["active_filters"].clear() + + return { + "success": True, + "message": f"Cleared {count} filter(s). Showing all {s['numNodes']} nodes and {s['numEdges']} edges.", + } + + +@mock_mcp.tool() +async def get_data_sample( + session_id: str, + table: str = "nodes", + limit: int = 5, +) -> Dict[str, Any]: + """ + Returns sample rows from the node or edge table as JSON. + Use to inspect data values before applying encodings or filters. + + Args: + session_id: The Graphistry session ID + table: 'nodes' or 'edges' + limit: Number of sample rows to return (1-10, default 5) + """ + s = _get_session(session_id) + limit = max(1, min(10, limit)) + + if table == "nodes": + rows = s["node_sample"][:limit] + columns = s["nodeColumns"] + elif table == "edges": + rows = s["edge_sample"][:limit] + columns = s["edgeColumns"] + else: + return {"error": f"Invalid table: {table}. Must be 'nodes' or 'edges'."} + + return { + "table": table, + "columns": columns, + "sample_rows": rows, + "total_count": s["numNodes"] if table == "nodes" else s["numEdges"], + "sample_count": len(rows), + } + + +def main() -> None: + """Run the mock viz MCP server.""" + mock_mcp.run() + + +if __name__ == "__main__": + main() From b719a9e5183b4ee14c4470bb6bbfd2044c337ffb Mon Sep 17 00:00:00 2001 From: silkspace Date: Wed, 18 Mar 2026 12:53:44 -0700 Subject: [PATCH 2/3] docs: Talk2Graph MCP design notes for Graphistry team Covers Louie integration (zero code changes needed), V1 tool surface recommendations, V2 collections/GFQL direction, and MCP routing guidance. --- TALK2GRAPH_MCP_DESIGN.md | 241 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 TALK2GRAPH_MCP_DESIGN.md diff --git a/TALK2GRAPH_MCP_DESIGN.md b/TALK2GRAPH_MCP_DESIGN.md new file mode 100644 index 0000000..1dd5036 --- /dev/null +++ b/TALK2GRAPH_MCP_DESIGN.md @@ -0,0 +1,241 @@ +# Talk2Graph: MCP Design Notes for the Graphistry Team + +This document covers how Louie (GraphistryGPT) integrates with the Talk2Graph MCP server in PR #3043, what works as-is, and design recommendations for the Graphistry team. + +## TL;DR + +- **Louie needs zero code changes.** The existing MCP plugin discovers tools dynamically from any MCP server URL. Just register the viz MCP endpoint as a connector. +- **The six tools in PR #3043 are the right V1 surface.** Add one more: `get_data_sample`. +- **Animation is Graphistry's concern.** Add an `animate` boolean to `set_encoding` so Louie can request it, but the implementation is entirely on the viz side. +- **V2 direction: collections + GFQL.** Instead of individual `set_encoding`/`add_filter` calls, a single `create_collection` tool that accepts a GFQL query and visual config. + +## Architecture: Two MCP Servers + +There are two separate MCP servers. They serve different purposes and should not be confused. + +### Graphistry Viz MCP (PR #3043, inside `streamgl-viz`) + +Runs per-worker inside the Graphistry product. Has direct access to live session state (`nBodiesById`, `workbooksById`, Falcor services). Manipulates the graph the user is currently looking at. + +### Graphistry PyGraphistry MCP (this repo, `graphistry-mcp`) + +Uses the PyGraphistry Python SDK to create new visualizations on hub.graphistry.com. For external LLM clients (Claude, Cursor, etc.) to build and analyze graphs from scratch. + +**Talk2Graph uses the viz MCP.** The PyGraphistry MCP is unrelated to this feature. + +## How Louie Connects + +Louie's `MCPPlugin` is registered at startup. It handles any MCP connector instance dynamically. When a connector URL is registered, Louie calls `list_tools()`, discovers all available tools, and generates typed Python methods with input validation. No manual tool registration. + +### Registering the connector + +**Option A: per-org (recommended for production)** + +Send a `CreateConnector` event to Louie: +```json +{ + "type": "CreateConnector", + "connector": { + "type": "MCP", + "name": "Graphistry Talk2Graph", + "description": "Graphistry viz session MCP tools", + "config": { + "server_url": "https:///mcp/", + "request_timeout_s": 120.0 + } + } +} +``` + +**Option B: system-wide (simpler for dev)** +```bash +MCP_SERVER_URL=https:///mcp/ +# Restart Louie — connector available to all users automatically +``` + +Both use existing Louie code paths. The `MCPConnectorConfig` only requires `server_url` (string) and `request_timeout_s` (float, default 120). + +## V1 Tool Surface: What to Keep, What to Add + +### Current tools (PR #3043) — all good + +| Tool | Status | Notes | +|------|--------|-------| +| `list_sessions` | Keep | Fine as-is | +| `get_session_info` | Keep, enhance | Add `verbosity` param (`brief`/`full`), include active encodings and active filters in the response | +| `set_encoding` | Keep, enhance | Add `animate: boolean` parameter (default false) | +| `reset_encoding` | Keep | Fine as-is | +| `add_filter` | Keep | The `addExpression` + `maskDataframe` approach is correct. Consider adding a `table` param (`point`/`edge`) to clarify scope | +| `reset_filters` | Keep | Fine as-is | + +### Add for V1: `get_data_sample` + +The LLM needs to see actual data values before it can suggest meaningful encodings or filters. Without this, it has to guess column semantics from names alone. + +```json +{ + "name": "get_data_sample", + "description": "Returns sample rows from the node or edge table as JSON. Use to inspect data values before applying encodings or filters.", + "inputSchema": { + "type": "object", + "properties": { + "session_id": { "type": "string" }, + "table": { "type": "string", "enum": ["nodes", "edges"] }, + "limit": { "type": "number", "minimum": 1, "maximum": 100, "default": 10 } + }, + "required": ["session_id", "table"] + } +} +``` + +### The `animate` flag + +From the design discussion: Louie decides what to change, Graphistry decides how to render it. The `animate` flag is an optional hint from the LLM. If you want to default-animate all MCP-driven changes regardless, that is fine too, but having the flag gives the LLM control when a user says "smoothly transition the colors" vs. "just color it." + +```json +{ + "session_id": "...", + "graph_type": "point", + "encoding_type": "color", + "attribute": "threat_score", + "animate": true +} +``` + +### `get_session_info` enhancements + +The LLM needs to know the current visual state to avoid redundant operations and to answer "what am I looking at?" questions. Include: + +```json +{ + "session_id": "abc123", + "numNodes": 1247, + "numEdges": 3891, + "title": "Enterprise Network Traffic", + "nodeColumns": ["ip_address", "hostname", "country", "threat_score", ...], + "edgeColumns": ["src_ip", "dst_ip", "protocol", "bytes", ...], + "active_encodings": { + "point_color": {"attribute": "threat_score"}, + "point_size": {"attribute": "degree"} + }, + "active_filters": [ + {"attribute": "threat_score", "operator": "gt", "value": 0.5} + ] +} +``` + +With `verbosity: "full"`, also include 3-5 sample rows per table so the LLM can see actual values. + +## MCP Routing + +Each PM2 worker runs its own MCP server. Louie registers one URL. The routing problem (reaching the right worker for a given session) is solved at the nginx/proxy layer, not inside Louie. + +Louie does not need to know about workers, PM2, or port assignments. It sends a `session_id` in every tool call, and the infrastructure routes to the correct worker. This is entirely the Graphistry team's problem to solve. + +Recommended options: +1. **Nginx route with `X-Session-ID` header** — Louie sends session ID, nginx routes to correct worker +2. **Single MCP gateway with Redis session lookup** — one endpoint proxies to correct worker +3. **Per-worker ports exposed directly** — simplest for single-node dev, does not scale + +For V1 single-node deployment, any of these work. + +## Session Context Optimization + +PR #3043 already does this correctly: on the first chat message, the viz backend gathers session context (columns, counts, metadata) and prepends it to the query before sending to Louie. This saves Louie from needing to call `get_session_info` on the first turn, cutting 2-5 seconds off the response time. + +The system prompt in `louieClient.js` tells the LLM to "only use column names that appear in the session context." This is the right constraint for V1. + +## V2 Direction: Collections + GFQL + +The current V1 pattern (individual `set_encoding`, `add_filter` calls) works but requires multiple round-trips for complex operations. The V2 goal is a single `create_collection` tool: + +```json +{ + "name": "create_collection", + "description": "Create a named collection with a GFQL query and visual configuration", + "inputSchema": { + "type": "object", + "properties": { + "session_id": { "type": "string" }, + "name": { "type": "string", "description": "Collection name" }, + "gfql_query": { "type": "string", "description": "GFQL query defining the collection" }, + "visual_config": { + "type": "object", + "properties": { + "point_color": { "type": "object", "properties": { "column": { "type": "string" }, "as_continuous": { "type": "boolean" } } }, + "point_size": { "type": "object", "properties": { "column": { "type": "string" }, "as_continuous": { "type": "boolean" } } }, + "animate": { "type": "boolean" } + } + } + }, + "required": ["session_id", "name", "gfql_query"] + } +} +``` + +Example: "select all nodes with PageRank > 0.3 and color them red" becomes: +```json +{ + "session_id": "abc123", + "name": "High PageRank Nodes", + "gfql_query": "MATCH (n) WITH n, pagerank(n) AS pr WHERE pr > 0.3 WITH n, hop(n, 1) AS nbr RETURN nbr", + "visual_config": { + "point_color": { "column": "pagerank", "as_continuous": true }, + "animate": true + } +} +``` + +This depends on the collections feature (Manfred's area) being ready for programmatic creation. Until then, V1 tools are sufficient. + +## V2: Algorithms as MCP Tools + +Expose graph algorithms that operate on the live session: + +```json +{ + "name": "run_algorithm", + "description": "Run a graph algorithm and add results as a new column", + "inputSchema": { + "type": "object", + "properties": { + "session_id": { "type": "string" }, + "algorithm": { "type": "string", "enum": ["pagerank", "betweenness", "community_detection", "degree"] }, + "output_column": { "type": "string", "description": "Name for the result column" } + }, + "required": ["session_id", "algorithm"] + } +} +``` + +The result column can then be used with `set_encoding` or `add_filter`, enabling chains like: run PageRank, then color by PageRank, then filter to high PageRank nodes. + +## Mock Server for Testing + +Branch `feat/talk2graph-mock-viz-mcp` in this repo contains a mock viz MCP server at: + +``` +src/graphistry_mcp_server/mock_viz_server.py +``` + +It implements all 7 V1 tools with two mock datasets (cybersecurity network traffic and social media influence graph). Useful for: + +- Testing Louie integration without a running Graphistry instance +- Validating tool discovery and argument schemas +- Frontend chat widget development against a predictable backend + +Run it: +```bash +# stdio mode (direct MCP client testing) +uv run python -m graphistry_mcp_server.mock_viz_server + +# HTTP mode (for Louie integration) +uv run fastmcp run src/graphistry_mcp_server/mock_viz_server.py --transport streamable-http --port 3100 +``` + +## Immediate Next Steps + +1. **Des + Manfred**: Review this doc, add `get_data_sample` tool and `animate` flag to `set_encoding` in PR #3043 +2. **Des + Manfred + Jared**: Short call to align on collections MCP design for V2 +3. **Jared**: Test Louie integration against mock server (register `http://localhost:3100/mcp` on local Louie) +4. **Des**: Record a Loom of the current Talk2Graph demo for async review From 37c9794c0f0df2ecf8badd617d6a952cc7eacf36 Mon Sep 17 00:00:00 2001 From: silkspace Date: Wed, 18 Mar 2026 13:15:35 -0700 Subject: [PATCH 3/3] docs: add system prompt fix and current status to design notes The biggest V1 UX issue is that Louie responds like an investigation agent (verbose, verdicts, evidence) instead of a concise graph assistant. Includes recommended TALK2GRAPH_SYSTEM_PROMPT replacement and prioritized next steps. --- TALK2GRAPH_MCP_DESIGN.md | 80 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/TALK2GRAPH_MCP_DESIGN.md b/TALK2GRAPH_MCP_DESIGN.md index 1dd5036..577e5b3 100644 --- a/TALK2GRAPH_MCP_DESIGN.md +++ b/TALK2GRAPH_MCP_DESIGN.md @@ -5,6 +5,8 @@ This document covers how Louie (GraphistryGPT) integrates with the Talk2Graph MC ## TL;DR - **Louie needs zero code changes.** The existing MCP plugin discovers tools dynamically from any MCP server URL. Just register the viz MCP endpoint as a connector. +- **Fix the system prompt first.** Louie responds like an investigation agent (verbose, verdicts, evidence). Update `TALK2GRAPH_SYSTEM_PROMPT` in `louieClient.js` to make it concise. This is the biggest UX win and it is a ~20 line change on the viz side. +- **Register the MCP server URL.** Tools are built but Louie cannot call them until the connector is registered. One env var or API call. - **The six tools in PR #3043 are the right V1 surface.** Add one more: `get_data_sample`. - **Animation is Graphistry's concern.** Add an `animate` boolean to `set_encoding` so Louie can request it, but the implementation is entirely on the viz side. - **V2 direction: collections + GFQL.** Instead of individual `set_encoding`/`add_filter` calls, a single `create_collection` tool that accepts a GFQL query and visual config. @@ -139,12 +141,60 @@ Recommended options: For V1 single-node deployment, any of these work. +## System Prompt: Critical UX Fix + +The current `TALK2GRAPH_SYSTEM_PROMPT` in `louieClient.js` is minimal, and because Louie defaults to its full investigation agent behavior, responses come back with verdict/evidence/investigation-style formatting that is way too verbose for a graph manipulation assistant. This is the single biggest UX issue to fix for V1. + +### The problem + +Louie's default `LouieAgent` is designed for deep investigations: it produces structured analysis with verdicts, evidence sections, confidence scores, and multi-paragraph explanations. That is exactly the wrong tone for Talk2Graph, where a user says "color by threat score" and expects a one-line confirmation, not a forensic report. + +### The fix + +Replace the system prompt in `louieClient.js` with something that constrains the response style. This is entirely on the viz side, no Louie changes needed. The system prompt is prepended to the user's query on the first message (before a dthread exists), so it sets the tone for the entire conversation. + +**Recommended prompt:** + +```javascript +const TALK2GRAPH_SYSTEM_PROMPT = `You are Talk2Graph, a concise graph visualization assistant embedded in Graphistry. + +RESPONSE STYLE: +- Answer in 1-3 sentences. Be brief and direct. +- When you use a tool, confirm what you did in one line: "Done — colored nodes by threat_score." +- Do NOT produce investigation reports, verdicts, evidence sections, or confidence scores. +- Do NOT use markdown headers, bullet lists, or structured analysis formats. +- If the user asks a question about the data, give a short factual answer based on the session context. + +TOOLS: +- You have MCP tools for manipulating the visualization (set_encoding, reset_encoding, add_filter, reset_filters, get_session_info, get_data_sample). +- Use these tools when the user wants to change colors, sizes, filters, or inspect data. +- Only use column names that appear in the session context. +- When using tools, always include the session_id from the session context. + +WHAT YOU ARE NOT: +- You are not an investigation agent. Do not analyze threats or produce reports. +- You are not a search engine. If you do not know something, say so briefly. +- You are a graph manipulation assistant. Help users see their data differently.`; +``` + +### Why this works without Louie changes + +The system prompt is injected by the viz backend in `buildQueryWithContext()` before the query reaches Louie. Louie's `LouieAgent` sees it as part of the user message on the first turn and follows the instructions. On subsequent turns (when a dthread exists), the conversation history carries the tone forward. No agent routing changes, no new agent type, no Louie PR needed. + +### V2: Dedicated Talk2Graph agent + +For V2, Louie should have a proper `Talk2GraphAgent` with its own prompt and tool routing, rather than relying on system prompt injection. This would: +- Use a lighter LLM (faster responses for simple operations) +- Have graph analytics domain knowledge built into the agent prompt +- Understand GFQL syntax for generating collection queries +- Skip the investigation pipeline entirely + +But for V1, the system prompt override in `louieClient.js` is the right lever and it is fast to ship. + ## Session Context Optimization PR #3043 already does this correctly: on the first chat message, the viz backend gathers session context (columns, counts, metadata) and prepends it to the query before sending to Louie. This saves Louie from needing to call `get_session_info` on the first turn, cutting 2-5 seconds off the response time. -The system prompt in `louieClient.js` tells the LLM to "only use column names that appear in the session context." This is the right constraint for V1. - ## V2 Direction: Collections + GFQL The current V1 pattern (individual `set_encoding`, `add_filter` calls) works but requires multiple round-trips for complex operations. The V2 goal is a single `create_collection` tool: @@ -233,9 +283,27 @@ uv run python -m graphistry_mcp_server.mock_viz_server uv run fastmcp run src/graphistry_mcp_server/mock_viz_server.py --transport streamable-http --port 3100 ``` +## Current Status (2026-03-18) + +What is working: +- Chat widget is in the right side panel, functional +- Chat proxy hits Louie, gets responses with session context +- Louie can answer questions about the graph (columns, structure, data) + +What is not working yet: +- **MCP server URL not registered** — Louie cannot call tools to change the visualization. This is just a config step (see "Registering the connector" above). +- **Responses are too verbose** — Louie produces investigation-style reports instead of brief confirmations. Fix by updating `TALK2GRAPH_SYSTEM_PROMPT` (see "System Prompt: Critical UX Fix" above). + ## Immediate Next Steps -1. **Des + Manfred**: Review this doc, add `get_data_sample` tool and `animate` flag to `set_encoding` in PR #3043 -2. **Des + Manfred + Jared**: Short call to align on collections MCP design for V2 -3. **Jared**: Test Louie integration against mock server (register `http://localhost:3100/mcp` on local Louie) -4. **Des**: Record a Loom of the current Talk2Graph demo for async review +**Priority 1 (unblocks everything):** +1. **Des**: Update `TALK2GRAPH_SYSTEM_PROMPT` in `louieClient.js` to the concise prompt above. This is the single biggest UX improvement and is a ~20 line change. +2. **Des or infra**: Register the MCP server URL on the Louie instance so tools actually work. Either set `MCP_SERVER_URL` env var or send `CreateConnector` event. + +**Priority 2 (V1 completeness):** +3. **Des + Manfred**: Add `get_data_sample` tool and `animate` flag to `set_encoding` in PR #3043 +4. **Des**: Record a Loom of the updated demo (concise responses + working tools) for async review + +**Priority 3 (V2 planning):** +5. **Des + Manfred + Jared**: Short call to align on `create_collection` MCP tool design for V2 +6. **Jared**: Test Louie integration against mock server in this repo