Skip to content

Commit 65e360b

Browse files
authored
Merge pull request galaxyproject#22011 from jmchilton/agent_plumbing
Refactor Agent Registry Wiring and Fix Config Options
2 parents ebf4dab + c27ac84 commit 65e360b

13 files changed

Lines changed: 177 additions & 172 deletions

File tree

lib/galaxy/agents/__init__.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
from .custom_tool import CustomToolAgent
1414
from .error_analysis import ErrorAnalysisAgent
1515
from .orchestrator import WorkflowOrchestratorAgent
16-
from .registry import AgentRegistry
16+
from .registry import (
17+
AgentRegistry,
18+
build_default_registry,
19+
)
1720
from .router import QueryRouterAgent
1821
from .tools import ToolRecommendationAgent
1922

@@ -22,19 +25,10 @@
2225
"BaseGalaxyAgent",
2326
"GalaxyAgentDependencies",
2427
"AgentRegistry",
28+
"build_default_registry",
2529
"QueryRouterAgent",
2630
"ErrorAnalysisAgent",
2731
"CustomToolAgent",
2832
"WorkflowOrchestratorAgent",
2933
"ToolRecommendationAgent",
3034
]
31-
32-
# Global agent registry instance
33-
agent_registry = AgentRegistry()
34-
35-
# Register default agents
36-
agent_registry.register(AgentType.ROUTER, QueryRouterAgent)
37-
agent_registry.register(AgentType.ERROR_ANALYSIS, ErrorAnalysisAgent)
38-
agent_registry.register(AgentType.CUSTOM_TOOL, CustomToolAgent)
39-
agent_registry.register(AgentType.ORCHESTRATOR, WorkflowOrchestratorAgent)
40-
agent_registry.register(AgentType.TOOL_RECOMMENDATION, ToolRecommendationAgent)

lib/galaxy/agents/base.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -222,13 +222,13 @@ class GalaxyAgentDependencies:
222222
trans: ProvidesUserContext
223223
user: User
224224
config: "GalaxyAppConfiguration"
225+
# Callable to get agent instances, avoids circular import in base.py
226+
get_agent: Callable[[str, "GalaxyAgentDependencies"], "BaseGalaxyAgent"]
225227
job_manager: Optional["JobManager"] = None
226228
dataset_manager: Optional["DatasetManager"] = None
227229
workflow_manager: Optional["WorkflowsManager"] = None
228230
tool_cache: Optional["ToolCache"] = None
229231
toolbox: Optional["ToolBox"] = None
230-
# Callable to get agent instances, avoids circular import in base.py
231-
get_agent: Optional[Callable[[str, "GalaxyAgentDependencies"], "BaseGalaxyAgent"]] = None
232232
# Optional factory for creating model instances (useful for testing)
233233
model_factory: Optional[Callable[[], Any]] = None
234234

@@ -720,10 +720,6 @@ async def _call_agent_from_tool(
720720
)
721721
"""
722722
try:
723-
if ctx.deps.get_agent is None:
724-
raise RuntimeError("get_agent not configured in dependencies")
725-
726-
# Get the target agent using the injected callable
727723
target_agent = ctx.deps.get_agent(agent_type, ctx.deps)
728724

729725
# Prepare query with context if available

lib/galaxy/agents/orchestrator.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -191,15 +191,13 @@ async def _execute_sequential(
191191
self, agents: list[str], query: str, context: Optional[dict[str, Any]] = None
192192
) -> dict[str, AgentResponse]:
193193
"""Execute agents sequentially with timeout protection."""
194-
from galaxy.agents import agent_registry
195-
196194
responses = {}
197195
current_query = query
198196
timeout = self._get_agent_timeout()
199197

200198
for agent_name in agents:
201199
try:
202-
agent = agent_registry.get_agent(agent_name, self.deps)
200+
agent = self.deps.get_agent(agent_name, self.deps)
203201
# Execute with timeout protection
204202
response = await asyncio.wait_for(agent.process(current_query, context or {}), timeout=timeout)
205203
responses[agent_name] = response
@@ -225,13 +223,11 @@ async def _execute_parallel(
225223
self, agents: list[str], query: str, context: Optional[dict[str, Any]] = None
226224
) -> dict[str, AgentResponse]:
227225
"""Execute agents in parallel with timeout protection."""
228-
from galaxy.agents import agent_registry
229-
230226
timeout = self._get_agent_timeout()
231227

232228
async def call_agent(agent_name: str):
233229
try:
234-
agent = agent_registry.get_agent(agent_name, self.deps)
230+
agent = self.deps.get_agent(agent_name, self.deps)
235231
# Execute with timeout protection
236232
response = await asyncio.wait_for(agent.process(query, context or {}), timeout=timeout)
237233
return agent_name, response

lib/galaxy/agents/registry.py

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -121,20 +121,46 @@ def list_agent_info(self) -> list[dict]:
121121
return [self.get_agent_info(agent_type) for agent_type in self._agents.keys()]
122122

123123

124-
# Global registry instance
125-
_global_registry = AgentRegistry()
126-
127-
128-
def get_global_registry() -> AgentRegistry:
129-
"""Get the global agent registry instance."""
130-
return _global_registry
131-
132-
133-
def register_agent(agent_type: str, agent_class: type[BaseGalaxyAgent], metadata: Optional[dict] = None):
134-
"""Register an agent in the global registry."""
135-
_global_registry.register(agent_type, agent_class, metadata)
136-
137-
138-
def get_agent(agent_type: str, deps: GalaxyAgentDependencies) -> BaseGalaxyAgent:
139-
"""Create an agent from the global registry."""
140-
return _global_registry.get_agent(agent_type, deps)
124+
def build_default_registry(config=None) -> AgentRegistry:
125+
"""Create an AgentRegistry with all default Galaxy agents.
126+
127+
Args:
128+
config: Optional app config. When provided, agents with
129+
``enabled: false`` in ``inference_services`` are skipped.
130+
The router agent is always registered regardless of config.
131+
"""
132+
from .base import AgentType
133+
from .custom_tool import CustomToolAgent
134+
from .error_analysis import ErrorAnalysisAgent
135+
from .orchestrator import WorkflowOrchestratorAgent
136+
from .router import QueryRouterAgent
137+
from .tools import ToolRecommendationAgent
138+
139+
inference_config: dict = {}
140+
if config is not None:
141+
inference_config = getattr(config, "inference_services", {}) or {}
142+
143+
def _is_enabled(agent_type: str) -> bool:
144+
agent_cfg = inference_config.get(agent_type, {})
145+
if isinstance(agent_cfg, dict):
146+
return agent_cfg.get("enabled", True)
147+
return True
148+
149+
def _register_if_enabled(registry: AgentRegistry, agent_type: str, agent_class: type[BaseGalaxyAgent]):
150+
if _is_enabled(agent_type):
151+
registry.register(agent_type, agent_class)
152+
else:
153+
log.info(f"Agent '{agent_type}' disabled by configuration, skipping registration")
154+
155+
registry = AgentRegistry()
156+
157+
# Router is always registered
158+
if not _is_enabled(AgentType.ROUTER):
159+
log.warning("Router agent cannot be disabled — ignoring enabled: false")
160+
registry.register(AgentType.ROUTER, QueryRouterAgent)
161+
162+
_register_if_enabled(registry, AgentType.ERROR_ANALYSIS, ErrorAnalysisAgent)
163+
_register_if_enabled(registry, AgentType.CUSTOM_TOOL, CustomToolAgent)
164+
_register_if_enabled(registry, AgentType.ORCHESTRATOR, WorkflowOrchestratorAgent)
165+
_register_if_enabled(registry, AgentType.TOOL_RECOMMENDATION, ToolRecommendationAgent)
166+
return registry

lib/galaxy/app.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
jobs,
2626
tools,
2727
)
28+
from galaxy.agents.registry import (
29+
AgentRegistry,
30+
build_default_registry,
31+
)
2832
from galaxy.carbon_emissions import get_carbon_intensity_entry
2933
from galaxy.celery.base_task import (
3034
GalaxyTaskBeforeStart,
@@ -44,6 +48,7 @@
4448
from galaxy.files.templates import ConfiguredFileSourceTemplates
4549
from galaxy.job_metrics import JobMetrics
4650
from galaxy.jobs.manager import JobManager
51+
from galaxy.managers.agents import AgentService
4752
from galaxy.managers.api_keys import ApiKeyManager
4853
from galaxy.managers.citations import CitationsManager
4954
from galaxy.managers.collections import DatasetCollectionManager
@@ -57,7 +62,10 @@
5762
from galaxy.managers.hdas import HDAManager
5863
from galaxy.managers.histories import HistoryManager
5964
from galaxy.managers.interactivetool import InteractiveToolManager
60-
from galaxy.managers.jobs import JobSearch
65+
from galaxy.managers.jobs import (
66+
JobManager as JobQueryManager,
67+
JobSearch,
68+
)
6169
from galaxy.managers.libraries import LibraryManager
6270
from galaxy.managers.library_datasets import LibraryDatasetsManager
6371
from galaxy.managers.notification import NotificationManager
@@ -641,6 +649,7 @@ def __init__(self, configure_logging=True, use_converters=True, use_display_appl
641649
self.library_datasets_manager = self._register_singleton(LibraryDatasetsManager)
642650
self.role_manager = self._register_singleton(RoleManager)
643651
self.job_manager = self._register_singleton(JobManager)
652+
644653
self.notification_manager = self._register_singleton(NotificationManager)
645654
self.interactivetool_manager = InteractiveToolManager(self)
646655

@@ -765,6 +774,11 @@ def __init__(self, **kwargs) -> None:
765774
# want to and we'll allow postfork to bind and start it.
766775
self.queue_worker = self._register_singleton(GalaxyQueueWorker, GalaxyQueueWorker(self))
767776

777+
# AI agent registry and service
778+
agent_registry = build_default_registry(self.config)
779+
self._register_singleton(AgentRegistry, agent_registry)
780+
self._register_singleton(AgentService, AgentService(self.config, JobQueryManager(self), agent_registry))
781+
768782
self.dependency_resolvers_view = self._register_singleton(
769783
DependencyResolversView, DependencyResolversView(self)
770784
)

lib/galaxy/config/__init__.py

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,7 +1141,6 @@ def _process_config(self, kwargs: dict[str, Any]) -> None:
11411141
# load in the chat_prompts if AI is configured (old key & base URL, or inference_services)
11421142
if self.ai_api_key or self.ai_api_base_url or getattr(self, "inference_services", None):
11431143
self._load_chat_prompts()
1144-
self._load_agent_config()
11451144

11461145
self.pretty_datetime_format = expand_pretty_datetime_format(self.pretty_datetime_format)
11471146
try:
@@ -1331,50 +1330,6 @@ def _load_chat_prompts(self):
13311330
else:
13321331
log.warning(f"Chat prompts file not found at {chat_prompts_path}")
13331332

1334-
def _load_agent_config(self):
1335-
"""Load agent configuration with defaults."""
1336-
# Set default agent configuration if not present
1337-
if not hasattr(self, "agents"):
1338-
self.agents: dict = {}
1339-
1340-
# Default agent configurations
1341-
default_agents = {
1342-
"router": {
1343-
"enabled": True,
1344-
"model": "openai:gpt-4",
1345-
"temperature": 0.3,
1346-
"max_tokens": 1000,
1347-
},
1348-
"error_analysis": {
1349-
"enabled": True,
1350-
"model": "openai:gpt-4",
1351-
"temperature": 0.2,
1352-
"max_tokens": 2000,
1353-
},
1354-
"dataset_analyzer": {
1355-
"enabled": False, # Beta feature
1356-
"model": "openai:gpt-4",
1357-
"temperature": 0.3,
1358-
"max_tokens": 1500,
1359-
},
1360-
"custom_tool": {
1361-
"enabled": True,
1362-
"model": "openai:gpt-4",
1363-
"temperature": 0.4,
1364-
"max_tokens": 2000,
1365-
},
1366-
}
1367-
1368-
# Merge with any existing config
1369-
for agent_type, default_config in default_agents.items():
1370-
if agent_type not in self.agents:
1371-
self.agents[agent_type] = default_config
1372-
else:
1373-
# Fill in missing keys with defaults
1374-
for key, value in default_config.items():
1375-
if key not in self.agents[agent_type]:
1376-
self.agents[agent_type][key] = value
1377-
13781333
def _process_celery_config(self):
13791334
if self.celery_conf and self.celery_conf.get("result_backend") is None:
13801335
# If the result_backend is not set, use a SQLite database in the data directory

lib/galaxy/config/sample/galaxy.yml.sample

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2993,10 +2993,12 @@ galaxy:
29932993
#ai_model: gpt-4o
29942994

29952995
# Configuration for AI inference services used by agents. Supports
2996-
# per-agent model, temperature, and token settings. Agents inherit
2997-
# from 'default' configuration, which itself falls back to global
2998-
# ai_model/ai_api_key settings. Example: inference_services: {
2999-
# default: { model: gpt-4o-mini, temperature: 0.7 } }
2996+
# per-agent model, temperature, max_tokens, api_key, api_base_url,
2997+
# and enabled settings. Agents inherit from 'default' configuration,
2998+
# which itself falls back to global ai_model/ai_api_key settings. All
2999+
# agents are enabled by default. Example: inference_services: {
3000+
# default: { model: gpt-4o-mini, temperature: 0.7 }, custom_tool: {
3001+
# enabled: false } }
30003002
#inference_services: null
30013003

30023004
# Allow the display of tool recommendations in workflow editor and

lib/galaxy/config/schemas/config_schema.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4086,10 +4086,11 @@ mapping:
40864086
required: false
40874087
desc: |
40884088
Configuration for AI inference services used by agents and visualization plugins.
4089-
Supports per-agent or per-plugin model, temperature, and token settings.
4089+
Supports per-agent or per-plugin model, temperature, max_tokens, api_key, api_base_url, and enabled settings.
40904090
Valid keys include agent types (e.g. router, error_analysis) and plugin names (e.g. jupyterlite).
40914091
Agents and plugins inherit from 'default' configuration, which itself falls back to global ai_model/ai_api_key settings.
4092-
Example: inference_services: { default: { model: gpt-4o-mini }, jupyterlite: { model: gpt-4o } }
4092+
All agents are enabled by default.
4093+
Example: inference_services: { default: { model: gpt-4o-mini, temperature: 0.7 }, custom_tool: { enabled: false }, jupyterlite: { model: gpt-4o } }
40934094
40944095
enable_tool_recommendations:
40954096
type: bool

lib/galaxy/dependencies/__init__.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -306,11 +306,6 @@ def check_tensorflow(self):
306306
def check_openai(self):
307307
return self.config.get("openai_api_key", None) is not None
308308

309-
def check_pydantic_ai(self):
310-
return (
311-
self.config.get("ai_api_key", None) is not None or self.config.get("inference_services", None) is not None
312-
)
313-
314309
def check_weasyprint(self):
315310
# See notes in ./conditional-requirements.txt for more information.
316311
return os.environ.get("GALAXY_DEPENDENCIES_INSTALL_WEASYPRINT") == "1"

lib/galaxy/managers/agents.py

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,15 @@
66
Optional,
77
)
88

9+
from galaxy.agents import GalaxyAgentDependencies
10+
from galaxy.agents.registry import AgentRegistry
11+
from galaxy.agents.router import QueryRouterAgent
912
from galaxy.config import GalaxyAppConfiguration
10-
from galaxy.exceptions import ConfigurationError
1113
from galaxy.managers.context import ProvidesUserContext
1214
from galaxy.managers.jobs import JobManager
1315
from galaxy.model import User
1416
from galaxy.schema.agents import AgentResponse
1517

16-
# Import agent system (pydantic_ai is optional)
17-
try:
18-
from galaxy.agents import (
19-
agent_registry,
20-
GalaxyAgentDependencies,
21-
)
22-
from galaxy.agents.error_analysis import ErrorAnalysisAgent
23-
from galaxy.agents.router import QueryRouterAgent
24-
25-
HAS_AGENTS = True
26-
except ImportError:
27-
HAS_AGENTS = False
28-
agent_registry = None # type: ignore[assignment,misc,unused-ignore]
29-
GalaxyAgentDependencies = None # type: ignore[assignment,misc,unused-ignore]
30-
QueryRouterAgent = None # type: ignore[assignment,misc,unused-ignore]
31-
ErrorAnalysisAgent = None # type: ignore[assignment,misc,unused-ignore]
32-
3318
log = logging.getLogger(__name__)
3419

3520

@@ -40,12 +25,11 @@ def __init__(
4025
self,
4126
config: GalaxyAppConfiguration,
4227
job_manager: JobManager,
28+
registry: AgentRegistry,
4329
):
44-
if not HAS_AGENTS:
45-
raise ConfigurationError("Agent system is not available")
46-
4730
self.config = config
4831
self.job_manager = job_manager
32+
self.registry = registry
4933

5034
def create_dependencies(self, trans: ProvidesUserContext, user: User) -> GalaxyAgentDependencies:
5135
"""Create agent dependencies for dependency injection."""
@@ -56,7 +40,7 @@ def create_dependencies(self, trans: ProvidesUserContext, user: User) -> GalaxyA
5640
config=self.config,
5741
job_manager=self.job_manager,
5842
toolbox=toolbox,
59-
get_agent=agent_registry.get_agent,
43+
get_agent=self.registry.get_agent,
6044
)
6145

6246
async def execute_agent(
@@ -75,7 +59,7 @@ async def execute_agent(
7559

7660
try:
7761
log.info(f"Executing {agent_type} agent for query: '{query[:100]}...'")
78-
agent = agent_registry.get_agent(agent_type, deps)
62+
agent = self.registry.get_agent(agent_type, deps)
7963
response = await agent.process(query, context)
8064

8165
return AgentResponse(
@@ -134,3 +118,9 @@ async def route_and_execute(
134118
# Explicit agent request - execute directly
135119
log.info(f"User explicitly requested agent: {agent_type}")
136120
return await self.execute_agent(agent_type, query, trans, user, context)
121+
122+
def list_agents(self) -> list[str]:
123+
return self.registry.list_agents()
124+
125+
def get_agent_info(self, agent_type: str) -> dict:
126+
return self.registry.get_agent_info(agent_type)

0 commit comments

Comments
 (0)