-
Notifications
You must be signed in to change notification settings - Fork 11
Use sys.executable for Python commands and add LLM config options #165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2f42cbf
3323636
e3cd8f3
fefaeea
75d5f52
c193a61
f6430a8
708b511
3ca2cc7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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_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 | ||
| 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.") | ||
|
Comment on lines
+62
to
+68
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder why ollama is treated here as a special case? just because it is offline?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct (this is the provider we tend to use the most often), so I added special functionality for auto starting ollama servers, pulling models, etc. But your comment actually did make me realize we don't need a device_property of ollama_model (since you can just do provider of ollama and chat_model_name of "gemma4:31b" example). |
||
|
|
||
| max_steps = attribute(label="Max Steps", dtype=int, access=tango.AttrWriteType.READ_WRITE) | ||
|
|
||
|
|
@@ -69,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] | ||
|
|
@@ -81,17 +93,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, | ||
| ) | ||
|
|
||
|
|
@@ -102,10 +120,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: | ||
|
|
@@ -126,8 +143,12 @@ 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, 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 +157,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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would give a sort of warning message to the user with list of models already there to choose from. More like: print("this model_x is not available, these models are available .....model_a, model_b..., either choose from these or Proceed as is to pull the model_x which may take some time")
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good thought! |
||
| 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, | ||
|
|
@@ -182,19 +214,16 @@ 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}}) | ||
| # 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(f"\n[SYSTEM]: Connecting to MCP Server at {url}...") | ||
| 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 | ||
|
|
@@ -245,7 +274,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) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just reading the notebook : prompt = "Get a scanned haadf image. Then get an EDS spectrum."
response = llm.query(prompt)
print(response)Recursion limit of 5 reached without hitting a stop condition. You can increase the limit by setting the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was a very interesting thing during testing actually. Basically what happened was the agent successfully acquired the image, and then tried to acquire an EDS spectrum. However, for some reason the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting. A issue on this would be nice. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If chat
chat_model_namehas default valuegpt-4otheapi_baseshould behttps://api.openai.com/v1?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
init_chat_modeldoesn't require anapi_basefor most providers (for example if you wanted to use openai you just set the provider to openai and the model name to gpt-4o and it automatically handles the routing).