From 2f42cbfe5b97fe1569b0562bb32bcf9a90266933 Mon Sep 17 00:00:00 2001 From: DomPTech Date: Fri, 7 Aug 2026 16:27:12 -0400 Subject: [PATCH 1/5] fix: use sys.executable for Python commands in `run_servers.py` instead of uv so they have their actual PIDs tracked --- startup_scripts/run_llm.py | 2 +- startup_scripts/run_servers.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/startup_scripts/run_llm.py b/startup_scripts/run_llm.py index 6434eae5..67b7e51b 100644 --- a/startup_scripts/run_llm.py +++ b/startup_scripts/run_llm.py @@ -96,7 +96,7 @@ def main(argv: list[str] | None = None) -> int: register_device(config) - command = ["uv", "run", "python", "-m", "asyncroscopy.mcp.llm", INSTANCE_NAME] + command = [sys.executable, "-m", "asyncroscopy.mcp.llm", INSTANCE_NAME] env = {**os.environ, 'TANGO_HOST': tango_host, 'PYTHONUNBUFFERED': '1'} try: diff --git a/startup_scripts/run_servers.py b/startup_scripts/run_servers.py index f5517a20..43646aa0 100755 --- a/startup_scripts/run_servers.py +++ b/startup_scripts/run_servers.py @@ -67,7 +67,7 @@ def device_name(self) -> str: @property def command(self) -> list[str]: - return ["uv", "run", "python", "-u", "-m", self.module_name, self.instance_name] + return [sys.executable, "-u", "-m", self.module_name, self.instance_name] @property def instance_name(self) -> str: @@ -592,7 +592,7 @@ def request_shutdown(_signum, _frame) -> None: database = manager.start_process( key="database", label="Tango database", - command=["uv", "run", "python", "-m", "tango.databaseds.database", "2"], + command=[sys.executable, "-m", "tango.databaseds.database", "2"], env=environment, ) print(" WAIT database readiness", end="", flush=True) From 332363684cdd53437ffffcccc853d12c451bb074 Mon Sep 17 00:00:00 2001 From: DomPTech Date: Fri, 7 Aug 2026 17:20:57 -0400 Subject: [PATCH 2/5] feat: new LLM config options for APIs and made ollama model config actually used (also downloads ollama model if not available); docs for LLM device properties --- asyncroscopy/mcp/llm.py | 57 ++++++++++++++++++++++++++++---------- configs/gemma-llm.yaml | 4 +-- startup_scripts/run_llm.py | 10 +++++-- 3 files changed, 51 insertions(+), 20 deletions(-) diff --git a/asyncroscopy/mcp/llm.py b/asyncroscopy/mcp/llm.py index 6f44e034..53ea536d 100644 --- a/asyncroscopy/mcp/llm.py +++ b/asyncroscopy/mcp/llm.py @@ -51,11 +51,21 @@ class AgentState(TypedDict): class LLM(Device): - mcp_url = device_property(dtype=str, default_value="http://127.0.0.1:8000/mcp") - startup_agents = device_property(dtype=(str,), default_value=()) - ollama_model = device_property(dtype=str, default_value="gemma4:31b") - use_init_chat_model = device_property(dtype=bool, default_value=False) - model_provider = device_property(dtype=str, default_value="ollama") + mcp_url = device_property(dtype=str, default_value="http://127.0.0.1:8000/mcp", doc="The URL of the MCP server to connect to.") + startup_agents = device_property(dtype=(str,), default_value=(), doc="List of JSON-serialized Agent configs to spawn on startup.") + + # Provider selection + use_init_chat_model = device_property(dtype=bool, default_value=False, doc="If true, use the init_chat_model function to initialize the LLM.") + model_provider = device_property(dtype=str, default_value="ollama", doc="The model provider to use for the LLM. Options: 'ollama', 'openai', etc.") + + # Generic init_chat_model config + chat_model_name = device_property(dtype=str, default_value="gpt-4o", doc="The name of the chat model to use for the LLM") + api_key = device_property(dtype=str, default_value="", doc="The API key for the model provider") + api_base = device_property(dtype=str, default_value="", doc="The base URL for the API") + + # Ollama config + ollama_model = device_property(dtype=str, default_value="gemma4:31b", doc="The Ollama model ID to use for the LLM") + auto_pull_model = device_property(dtype=bool, default_value=True, doc="If true, automatically pull the Ollama model if it is not already downloaded.") max_steps = attribute(label="Max Steps", dtype=int, access=tango.AttrWriteType.READ_WRITE) @@ -81,17 +91,23 @@ async def init_device(self) -> None: if self.use_init_chat_model: # Initialize from most model providers (e.g., OpenAI) self.info_stream("Initializing via init_chat_model") - self._model = init_chat_model( - model=self.ollama_model, - model_provider=self.model_provider, - temperature=0 - ) + + model_kwargs = { + "model": self.chat_model_name, + "model_provider": self.model_provider + } + if self.api_key: + model_kwargs["api_key"] = self.api_key + if self.api_base: + model_kwargs["api_base"] = self.api_base + + self._model = init_chat_model(**model_kwargs) else: # Initialize locally via Ollama from langchain_ollama import ChatOllama self.info_stream("Initializing via ChatOllama") self._model = ChatOllama( model=self.ollama_model, - temperature=0, + temperature=0.7, reasoning=False, ) @@ -127,7 +143,7 @@ def agents(self) -> list[str]: return [agent.name for agent in self._agents] async def ensure_ollama_running(self, host: str = "http://localhost:11434", timeout: int = 10) -> None: - """Check if Ollama server is running, offloaded to prevent blocking the Tango loop.""" + """Check if Ollama server is running, starting it and downloading the model if necessary.""" def _sync_check(): tags_url = f"{host.rstrip('/')}/api/tags" @@ -136,8 +152,19 @@ def _sync_check(): return except (urllib.error.URLError, TimeoutError, ConnectionRefusedError): pass - - try: + + if self.auto_pull_model: # Run command to download the model if it is not already + print(f"[SYSTEM]: Ensuring model '{self.ollama_model}' is pulled (this may take a while if it is not already downloaded)...") + try: + subprocess.run( + ["ollama", "pull", self.ollama_model], + check=True, + stderr=subprocess.DEVNULL + ) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Failed to pull Ollama model {self.ollama_model}: {e}") + + try: # Run command to serve the model subprocess.Popen( ["ollama", "serve"], stdout=subprocess.DEVNULL, @@ -245,7 +272,7 @@ def _extract_json(self, text: str) -> str: return text.strip() def _parse_routing_decision(self, content: str, valid_options: list[str], fallback: str) -> tuple[str, str]: - """Parse a supervisor response's {'next': ...} decision, falling back on any error or invalid value.""" + """Parse a supervisor response's {'next': ...} decision with a fallback.""" try: decision = json.loads(self._extract_json(content)) next_agent = decision.get("next", fallback) diff --git a/configs/gemma-llm.yaml b/configs/gemma-llm.yaml index 793ce53d..cc468d1d 100644 --- a/configs/gemma-llm.yaml +++ b/configs/gemma-llm.yaml @@ -4,8 +4,8 @@ tango: host: localhost port: 9094 -mcp_url: "http://127.0.0.1:8001/mcp" -local_model_path: C:\Users\Public\Desktop\Agents\Gemma4-31B-4bit +mcp_url: "http://127.0.0.1:8000/mcp" +ollama_model: gemma4:31b startup_agents: - name: "base" diff --git a/startup_scripts/run_llm.py b/startup_scripts/run_llm.py index 67b7e51b..19da38fc 100644 --- a/startup_scripts/run_llm.py +++ b/startup_scripts/run_llm.py @@ -31,10 +31,14 @@ class TangoConfig: class LLMConfig: tango: TangoConfig mcp_url: str - local_model_path: str | None = None - model_provider: str | None = None - model_name: str | None = None + + chat_model_name: str | None = None api_key: str | None = None + api_base: str | None = None + + ollama_model: str | None = None + auto_pull_model: bool = True + startup_agents: list[Agent] | None = None def __post_init__(self): From c193a61ddb8033595c0fd15144bf29be1a930094 Mon Sep 17 00:00:00 2001 From: DomPTech Date: Mon, 10 Aug 2026 15:26:17 -0400 Subject: [PATCH 3/5] docs: Added LLM Device doc --- docs/MCP/llm_device.md | 36 ++++++++++++++++++++++++++++++++++++ docs/index.md | 1 + 2 files changed, 37 insertions(+) create mode 100644 docs/MCP/llm_device.md diff --git a/docs/MCP/llm_device.md b/docs/MCP/llm_device.md new file mode 100644 index 00000000..166bb2d8 --- /dev/null +++ b/docs/MCP/llm_device.md @@ -0,0 +1,36 @@ +# LLM Device + +The `LLM` device is a Tango device that wraps an LangChain AI agent swarm. It enables AI agents to interact with the hardware tools exposed via the MCP server. It is started with `startup_scripts/run_llm.py`. + +## Overview + +The device initializes a LangChain/LangGraph-based swarm. It exposes Tango Commands to connect to multiple MCP servers, spawn specialized worker agents, and take queries. A central Supervisor agent routes tasks to the appropriate worker based on user input. + +This Device may be initialized with a local model, via Ollama, or more generally via Langchain's `init_chat_model` method that takes a model and provider, and optionally other args/kwargs like an API key. With Ollama, the model is automatically served if the server isn't already running and optionally pulled if not already downloaed. + +## Commands + +- **`SpawnAgent(config: str)`** + Creates a new worker agent. Expects a JSON-serialized string: + ```json + { + "name": "agent_name", + "system_prompt": "Agent role and instructions.", + "description": "An optional description to help the Supervisor with routing", + "model": "optional_model_override", + "tools": ["glob_pattern_1", "glob_pattern_2"] + } + ``` + Glob patterns can be used for giving agents multiple related tools, such as "*image" to give it all tools whose names end in the word "image". + +- **`ConnectMCP(config: str)`** + Connects to an MCP server and inherits its tools. Expects a JSON string: + ```json + { + "url": "http://127.0.0.1:8000/mcp", + "transport": "streamable_http" + } + ``` + +- **`Query(prompt: str)`** + Queries the swarm. If more than one agent is in the swarm, a Supervisor routes this prompt to the appropriate agent. Otherwise, the single agent handles the request like a normal chatbot. diff --git a/docs/index.md b/docs/index.md index 5f2aa87b..26efe20c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,6 +30,7 @@ Use this site to navigate contributor guidance, microscope architecture notes, h - [Add a Detector](Adding_New_Hardware/add_detector.md): detector onboarding checklist and implementation notes. - [Data Integration (Tiled)](Tiled_server/data_integration.md): how acquisitions are saved, registered, and served via the DATA device and Tiled. - [MCP Server Documentation](MCP/mcp_server.md): how Tango commands are exposed to MCP-compatible agents. +- [LLM Device Documentation](MCP/llm_device.md): configuring the agent swarm and MCP tool integration. ## Operation From f6430a867dda9d288447e05cfa0cb0e628cf1df1 Mon Sep 17 00:00:00 2001 From: DomPTech Date: Mon, 10 Aug 2026 15:37:49 -0400 Subject: [PATCH 4/5] fix: replace mcp_url property in LLM Device with mcp_config to support more transport protocols --- asyncroscopy/mcp/llm.py | 15 ++++++--------- configs/gemma-llm.yaml | 4 +++- startup_scripts/run_llm.py | 22 ++++++++++------------ 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/asyncroscopy/mcp/llm.py b/asyncroscopy/mcp/llm.py index 53ea536d..80821e39 100644 --- a/asyncroscopy/mcp/llm.py +++ b/asyncroscopy/mcp/llm.py @@ -51,7 +51,7 @@ class AgentState(TypedDict): class LLM(Device): - mcp_url = device_property(dtype=str, default_value="http://127.0.0.1:8000/mcp", doc="The URL of the MCP server to connect to.") + mcp_config = device_property(dtype=str, doc="JSON-serialized config of the MCP server to initially connect to.") startup_agents = device_property(dtype=(str,), default_value=(), doc="List of JSON-serialized Agent configs to spawn on startup.") # Provider selection @@ -118,10 +118,9 @@ async def init_device(self) -> None: print(f"[SYSTEM]: Model pre-warmed in {time.time() - start_warmup:.2f}s!") # Connect to an MCP server initially if specified - if self.mcp_url: - config = json.dumps({"url": self.mcp_url, "transport": "streamable_http"}) - if not await self.ConnectMCP(config): - print(f"[SYSTEM]: Failed to connect to MCP Server at {self.mcp_url}.") + if self.mcp_config: + if not await self.ConnectMCP(self.mcp_config): + print(f"[SYSTEM]: Failed to connect to MCP Server: {self.mcp_config}.") self.set_state(tango.DevState.ON) except Exception as e: @@ -209,13 +208,11 @@ async def ConnectMCP(self, config: str) -> bool: """Connect to an MCP server and inherit its tools. Returns true for success.""" try: args = json.loads(config) - url = args.get("url") - transport = args.get("transport", "streamable_http") server_id = f"server_{len(self._mcp_clients)}" - client = MultiServerMCPClient({server_id: {"url": url, "transport": transport}}) + client = MultiServerMCPClient({server_id: args}) - print(f"\n[SYSTEM]: Connecting to MCP Server at {url}...") + print("\n[SYSTEM]: Connecting to MCP Server...") tools = await client.get_tools() diff --git a/configs/gemma-llm.yaml b/configs/gemma-llm.yaml index cc468d1d..82260baf 100644 --- a/configs/gemma-llm.yaml +++ b/configs/gemma-llm.yaml @@ -4,7 +4,9 @@ tango: host: localhost port: 9094 -mcp_url: "http://127.0.0.1:8000/mcp" +mcp_config: + url: "http://127.0.0.1:8000/mcp" + transport: streamable-http ollama_model: gemma4:31b startup_agents: diff --git a/startup_scripts/run_llm.py b/startup_scripts/run_llm.py index 19da38fc..036549e2 100644 --- a/startup_scripts/run_llm.py +++ b/startup_scripts/run_llm.py @@ -1,3 +1,4 @@ +from typing import Literal import json import sys import argparse @@ -27,10 +28,15 @@ class TangoConfig: host: str port: int +@dataclass +class MCPConfig: + url: str | None = None + transport: Literal["stdio", "http", "sse", "streamable-http"] = "streamable-http" + @dataclass class LLMConfig: tango: TangoConfig - mcp_url: str + mcp_config: MCPConfig chat_model_name: str | None = None api_key: str | None = None @@ -41,16 +47,6 @@ class LLMConfig: startup_agents: list[Agent] | None = None - def __post_init__(self): - # Convert tango dict to TangoConfig - if isinstance(self.tango, dict): - self.tango = TangoConfig(**self.tango) - -def _require(mapping: dict, key: str, where: str): - if not isinstance(mapping, dict) or key not in mapping: - raise KeyError(f"Config section '{where}' is missing required key '{key}'") - return mapping[key] - def load_config(path: Path) -> LLMConfig: if not path.exists(): @@ -77,6 +73,8 @@ def register_device(config: LLMConfig | None): properties[key] = value if key == "startup_agents": properties[key] = [json.dumps(agent) for agent in value] + elif key == "mcp_config": + properties[key] = json.dumps(value) database.put_device_property(DEVICE_NAME, properties) print(f"Set device properties: {properties}") @@ -95,7 +93,7 @@ def main(argv: list[str] | None = None) -> int: print(f'Config error: {exc}', file=sys.stderr) return 1 - tango_host = f'{config.tango.host}:{config.tango.port}' + tango_host = f'{config.tango["host"]}:{config.tango["port"]}' os.environ['TANGO_HOST'] = tango_host register_device(config) From 708b511b846249f262beb1a10dc063a117531ec5 Mon Sep 17 00:00:00 2001 From: DomPTech Date: Mon, 10 Aug 2026 17:30:10 -0400 Subject: [PATCH 5/5] feat: LLM Device uses a single, updating instance of a MultiServerMCPClient instead of multiple; added `mcp_connections` attribute --- asyncroscopy/mcp/llm.py | 23 ++++++++++------- configs/mcp_dt.yaml | 2 +- notebooks/11_Test_AI_Agent.ipynb | 43 ++++++++++---------------------- 3 files changed, 28 insertions(+), 40 deletions(-) diff --git a/asyncroscopy/mcp/llm.py b/asyncroscopy/mcp/llm.py index 80821e39..24990a5c 100644 --- a/asyncroscopy/mcp/llm.py +++ b/asyncroscopy/mcp/llm.py @@ -51,7 +51,7 @@ class AgentState(TypedDict): class LLM(Device): - mcp_config = device_property(dtype=str, doc="JSON-serialized config of the MCP server to initially connect to.") + mcp_config = device_property(dtype=str, doc="JSON-serialized config of the MCP server to initially connect to: {'url': '...', 'transport': '...'}.") startup_agents = device_property(dtype=(str,), default_value=(), doc="List of JSON-serialized Agent configs to spawn on startup.") # Provider selection @@ -79,7 +79,9 @@ async def init_device(self) -> None: # Registries self._agents: list[Agent] = [] self._tools: list[BaseTool] = [] - self._mcp_clients: list[MultiServerMCPClient] = [] + + # Manages MCP connections + self._mcp_client = MultiServerMCPClient({}) if self.startup_agents: self._agents = [Agent(**json.loads(agent_json)) for agent_json in self.startup_agents] @@ -141,6 +143,10 @@ def agents(self) -> list[str]: """Return a list of the names of all currently spawned agents.""" return [agent.name for agent in self._agents] + @attribute(dtype=str) + def mcp_connections(self) -> str: + return str(self._mcp_client.connections) + async def ensure_ollama_running(self, host: str = "http://localhost:11434", timeout: int = 10) -> None: """Check if Ollama server is running, starting it and downloading the model if necessary.""" @@ -209,16 +215,15 @@ async def ConnectMCP(self, config: str) -> bool: try: args = json.loads(config) - server_id = f"server_{len(self._mcp_clients)}" - client = MultiServerMCPClient({server_id: args}) + # Generate unique ID for this connection and add it to the client + server_id = f"server_{len(self._mcp_client.connections)}" + self._mcp_client.connections[server_id] = args - print("\n[SYSTEM]: Connecting to MCP Server...") + print(f"\n[SYSTEM]: Added MCP server {server_id}. Fetching tools...") - tools = await client.get_tools() + self._tools = await self._mcp_client.get_tools() - self._mcp_clients.append(client) - self._tools.extend(tools) - print(f"[SYSTEM]: Connected. Inherited {len(tools)} tools.") + print(f"[SYSTEM]: Connected. Inherited {len(self._tools)} tools.") except Exception as e: self.error_stream(f"Failed to connect to MCP server: {e}") return False diff --git a/configs/mcp_dt.yaml b/configs/mcp_dt.yaml index c59e72e7..576d9f83 100644 --- a/configs/mcp_dt.yaml +++ b/configs/mcp_dt.yaml @@ -8,7 +8,7 @@ # machine/IP. Set mcp.http_host to 0.0.0.0 when other machines need to connect. tango: - host: 127.0.0.1 + host: localhost port: 9094 mcp: diff --git a/notebooks/11_Test_AI_Agent.ipynb b/notebooks/11_Test_AI_Agent.ipynb index 7726e412..63c54b42 100644 --- a/notebooks/11_Test_AI_Agent.ipynb +++ b/notebooks/11_Test_AI_Agent.ipynb @@ -43,7 +43,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 18, "id": "c4d20080", "metadata": {}, "outputs": [], @@ -65,7 +65,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "id": "04f5aae1", "metadata": {}, "outputs": [ @@ -76,22 +76,9 @@ "localhost:9094\n", "asyncroscopy/instrument/default ON\n", "asyncroscopy/data/default ON\n", - "asyncroscopy/eds/default ON\n", + "asyncroscopy/llm/default ON\n", "Tiled: http://localhost:9091\n" ] - }, - { - "ename": "AttributeError", - "evalue": "acquire_spectrum", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mAttributeError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[22]\u001b[39m\u001b[32m, line 21\u001b[39m\n\u001b[32m 17\u001b[39m tiled_config = json.loads(data.get_config())\n\u001b[32m 18\u001b[39m client = from_uri(tiled_config[\u001b[33m\"uri\"\u001b[39m])\n\u001b[32m 19\u001b[39m print(\u001b[33m\"Tiled:\"\u001b[39m, tiled_config[\u001b[33m\"uri\"\u001b[39m])\n\u001b[32m 20\u001b[39m \n\u001b[32m---> \u001b[39m\u001b[32m21\u001b[39m eds.acquire_spectrum(\u001b[33m\"EDS\"\u001b[39m)\n", - "\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\dpelaia\\Documents\\asyncroscopy-agent-version\\.venv\\Lib\\site-packages\\tango\\device_proxy.py:2319\u001b[39m, in \u001b[36m__safe_call..safe_call_wrapper\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 2317\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mself\u001b[39m._initialized:\n\u001b[32m 2318\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\u001b[33m\"\u001b[39m\u001b[33mDeviceProxy object was not fully initialized.\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m-> \u001b[39m\u001b[32m2319\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[30;43mfn\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43margs\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43mkwargs\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n", - "\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\dpelaia\\Documents\\asyncroscopy-agent-version\\.venv\\Lib\\site-packages\\tango\\device_proxy.py:484\u001b[39m, in \u001b[36m__DeviceProxy__getattr\u001b[39m\u001b[34m(self, name)\u001b[39m\n\u001b[32m 481\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m attr_info:\n\u001b[32m 482\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m __get_attribute_value(\u001b[38;5;28mself\u001b[39m, attr_info, name)\n\u001b[32m--> \u001b[39m\u001b[32m484\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mAttributeError\u001b[39;00m(name) \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mcause\u001b[39;00m\n\u001b[32m 485\u001b[39m \u001b[38;5;28;01mfinally\u001b[39;00m:\n\u001b[32m 486\u001b[39m \u001b[38;5;28;01mdel\u001b[39;00m cause\n", - "\u001b[31mAttributeError\u001b[39m: acquire_spectrum" - ] } ], "source": [ @@ -117,7 +104,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 20, "id": "ce4eba9c", "metadata": {}, "outputs": [ @@ -127,7 +114,7 @@ "True" ] }, - "execution_count": 17, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -147,7 +134,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 21, "id": "1e1e508a", "metadata": {}, "outputs": [ @@ -155,12 +142,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "('base', 'image', 'eds')\n" + "('base', 'image', 'eds')\n", + "{'server_0': {'url': 'http://127.0.0.1:8000/mcp', 'transport': 'streamable-http'}}\n" ] } ], "source": [ - "print(llm.agents)" + "print(llm.agents)\n", + "print(llm.mcp_connections)" ] }, { @@ -173,7 +162,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 22, "id": "a1e65cdd", "metadata": {}, "outputs": [ @@ -181,14 +170,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "The available devices are:\n", - "- asyncroscopy/camera/default\n", - "- asyncroscopy/data/default\n", - "- asyncroscopy/eds/default\n", - "- asyncroscopy/instrument/default\n", - "- asyncroscopy/llm/default\n", - "- asyncroscopy/scan/default\n", - "- asyncroscopy/stage/default\n" + "Recursion limit of 5 reached without hitting a stop condition. You can increase the limit by setting the `recursion_limit` config key.\n", + "For troubleshooting, visit: https://docs.langchain.com/oss/python/langgraph/errors/GRAPH_RECURSION_LIMIT\n" ] } ],