Skip to content

Commit 6c7bb36

Browse files
committed
Replace --triage flag with --agent option for query command
- Add --agent/-a option with choices: auto, triage, chat, knowledge - Default to 'auto' which uses the router to select the agent - Update help text to document all agent types - Add comprehensive tests for each agent selection mode - Regenerate CLI reference docs
1 parent 4a85ba2 commit 6c7bb36

File tree

3 files changed

+199
-19
lines changed

3 files changed

+199
-19
lines changed

docs/reference/cli.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,11 @@ Generated from the Click command tree.
7373
to continue an existing conversation, or omit it to start a new one.
7474

7575

76-
The agent is automatically selected based on the query:
77-
- Knowledge agent: General Redis questions (no instance)
78-
- Chat agent: Quick questions with a Redis instance
79-
- Triage agent: Full health checks or --triage flag
76+
The agent is automatically selected based on the query, or use --agent:
77+
- knowledge: General Redis questions (no instance needed)
78+
- chat: Quick questions with a Redis instance
79+
- triage: Full health checks and diagnostics
80+
- auto: Let the router decide (default)
8081
- worker — Start the background worker.
8182
- mcp — MCP server commands - expose agent capabilities via Model Context Protocol.
8283
- mcp list-tools — List available MCP tools.

redis_sre_agent/cli/query.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,25 @@
2424
@click.argument("query")
2525
@click.option("--redis-instance-id", "-r", help="Redis instance ID to investigate")
2626
@click.option("--thread-id", "-t", help="Thread ID to continue an existing conversation")
27-
@click.option("--triage", is_flag=True, help="Force full triage agent (bypasses routing)")
28-
def query(query: str, redis_instance_id: Optional[str], thread_id: Optional[str], triage: bool):
27+
@click.option(
28+
"--agent",
29+
"-a",
30+
type=click.Choice(["auto", "triage", "chat", "knowledge"], case_sensitive=False),
31+
default="auto",
32+
help="Agent to use (default: auto-select based on query)",
33+
)
34+
def query(query: str, redis_instance_id: Optional[str], thread_id: Optional[str], agent: str):
2935
"""Execute an agent query.
3036
3137
Supports conversation threads for multi-turn interactions. Use --thread-id
3238
to continue an existing conversation, or omit it to start a new one.
3339
3440
\b
35-
The agent is automatically selected based on the query:
36-
- Knowledge agent: General Redis questions (no instance)
37-
- Chat agent: Quick questions with a Redis instance
38-
- Triage agent: Full health checks or --triage flag
41+
The agent is automatically selected based on the query, or use --agent:
42+
- knowledge: General Redis questions (no instance needed)
43+
- chat: Quick questions with a Redis instance
44+
- triage: Full health checks and diagnostics
45+
- auto: Let the router decide (default)
3946
"""
4047

4148
async def _query():
@@ -100,10 +107,18 @@ async def _query():
100107
# Build context for routing
101108
routing_context = {"instance_id": instance.id} if instance else None
102109

110+
# Map CLI agent choice to AgentType
111+
agent_choice_map = {
112+
"triage": AgentType.REDIS_TRIAGE,
113+
"chat": AgentType.REDIS_CHAT,
114+
"knowledge": AgentType.KNOWLEDGE_ONLY,
115+
}
116+
103117
# Determine which agent to use
104-
if triage:
105-
agent_type = AgentType.REDIS_TRIAGE
106-
console.print("[dim]🔧 Agent: Triage (forced)[/dim]")
118+
if agent != "auto":
119+
agent_type = agent_choice_map[agent.lower()]
120+
agent_label = agent.capitalize()
121+
console.print(f"[dim]🔧 Agent: {agent_label} (selected)[/dim]")
107122
else:
108123
agent_type = await route_to_appropriate_agent(
109124
query=query,
@@ -116,19 +131,19 @@ async def _query():
116131
}.get(agent_type, agent_type.value)
117132
console.print(f"[dim]🔧 Agent: {agent_label}[/dim]")
118133

119-
# Get the appropriate agent
134+
# Get the appropriate agent instance
120135
if agent_type == AgentType.REDIS_TRIAGE:
121-
agent = get_sre_agent()
136+
selected_agent = get_sre_agent()
122137
elif agent_type == AgentType.REDIS_CHAT:
123-
agent = get_chat_agent(redis_instance=instance)
138+
selected_agent = get_chat_agent(redis_instance=instance)
124139
else:
125-
agent = get_knowledge_agent()
140+
selected_agent = get_knowledge_agent()
126141

127142
try:
128143
context = {"instance_id": instance.id} if instance else None
129144

130145
# Run the agent
131-
response = await agent.process_query(
146+
response = await selected_agent.process_query(
132147
query,
133148
session_id="cli",
134149
user_id="cli_user",

tests/unit/cli/test_cli_query.py

Lines changed: 165 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,19 @@
77
from redis_sre_agent.cli.query import query
88

99

10-
def test_query_cli_help_shows_instance_option():
10+
def test_query_cli_help_shows_options():
1111
runner = CliRunner()
1212
result = runner.invoke(query, ["--help"])
1313

1414
assert result.exit_code == 0
1515
assert "--redis-instance-id" in result.output
1616
assert "-r" in result.output
17+
assert "--agent" in result.output
18+
assert "-a" in result.output
19+
assert "auto" in result.output
20+
assert "triage" in result.output
21+
assert "chat" in result.output
22+
assert "knowledge" in result.output
1723

1824

1925
def test_query_without_instance_uses_knowledge_agent():
@@ -144,3 +150,161 @@ def test_query_with_unknown_instance_exits_with_error_and_skips_agents():
144150
mock_get_knowledge.assert_not_called()
145151
mock_get_sre.assert_not_called()
146152
mock_agent.process_query.assert_not_awaited()
153+
154+
155+
def test_query_with_agent_triage_forces_triage_agent():
156+
"""Test that --agent triage forces use of the triage agent."""
157+
runner = CliRunner()
158+
159+
mock_agent = MagicMock()
160+
mock_agent.process_query = AsyncMock(return_value="triage result")
161+
162+
with (
163+
patch("redis_sre_agent.cli.query.get_sre_agent", return_value=mock_agent) as mock_get_sre,
164+
patch("redis_sre_agent.cli.query.get_knowledge_agent") as mock_get_knowledge,
165+
patch("redis_sre_agent.cli.query.get_chat_agent") as mock_get_chat,
166+
patch("redis_sre_agent.cli.query.route_to_appropriate_agent") as mock_router,
167+
):
168+
result = runner.invoke(query, ["--agent", "triage", "Check my Redis health"])
169+
170+
assert result.exit_code == 0, result.output
171+
assert "Triage (selected)" in result.output
172+
173+
# Triage agent should be used
174+
mock_get_sre.assert_called_once()
175+
mock_get_knowledge.assert_not_called()
176+
mock_get_chat.assert_not_called()
177+
178+
# Router should NOT be called when agent is explicitly specified
179+
mock_router.assert_not_called()
180+
181+
mock_agent.process_query.assert_awaited_once()
182+
183+
184+
def test_query_with_agent_knowledge_forces_knowledge_agent():
185+
"""Test that --agent knowledge forces use of the knowledge agent."""
186+
runner = CliRunner()
187+
188+
mock_agent = MagicMock()
189+
mock_agent.process_query = AsyncMock(return_value="knowledge result")
190+
191+
with (
192+
patch(
193+
"redis_sre_agent.cli.query.get_knowledge_agent", return_value=mock_agent
194+
) as mock_get_knowledge,
195+
patch("redis_sre_agent.cli.query.get_sre_agent") as mock_get_sre,
196+
patch("redis_sre_agent.cli.query.get_chat_agent") as mock_get_chat,
197+
patch("redis_sre_agent.cli.query.route_to_appropriate_agent") as mock_router,
198+
):
199+
result = runner.invoke(query, ["-a", "knowledge", "What is Redis replication?"])
200+
201+
assert result.exit_code == 0, result.output
202+
assert "Knowledge (selected)" in result.output
203+
204+
# Knowledge agent should be used
205+
mock_get_knowledge.assert_called_once()
206+
mock_get_sre.assert_not_called()
207+
mock_get_chat.assert_not_called()
208+
209+
# Router should NOT be called
210+
mock_router.assert_not_called()
211+
212+
mock_agent.process_query.assert_awaited_once()
213+
214+
215+
def test_query_with_agent_chat_forces_chat_agent():
216+
"""Test that --agent chat forces use of the chat agent."""
217+
runner = CliRunner()
218+
219+
class DummyInstance:
220+
def __init__(self):
221+
self.id = "test-instance"
222+
self.name = "Test Instance"
223+
self.instance_type = "oss_single"
224+
self.connection_url = "redis://localhost:6379"
225+
self.environment = "development"
226+
self.usage = "cache"
227+
228+
instance = DummyInstance()
229+
230+
mock_agent = MagicMock()
231+
mock_agent.process_query = AsyncMock(return_value="chat result")
232+
233+
with (
234+
patch("redis_sre_agent.cli.query.get_chat_agent", return_value=mock_agent) as mock_get_chat,
235+
patch("redis_sre_agent.cli.query.get_sre_agent") as mock_get_sre,
236+
patch("redis_sre_agent.cli.query.get_knowledge_agent") as mock_get_knowledge,
237+
patch(
238+
"redis_sre_agent.cli.query.get_instance_by_id",
239+
new=AsyncMock(return_value=instance),
240+
),
241+
patch("redis_sre_agent.cli.query.route_to_appropriate_agent") as mock_router,
242+
):
243+
result = runner.invoke(query, ["--agent", "chat", "-r", "test-instance", "Quick question"])
244+
245+
assert result.exit_code == 0, result.output
246+
assert "Chat (selected)" in result.output
247+
248+
# Chat agent should be used
249+
mock_get_chat.assert_called_once()
250+
mock_get_sre.assert_not_called()
251+
mock_get_knowledge.assert_not_called()
252+
253+
# Router should NOT be called
254+
mock_router.assert_not_called()
255+
256+
mock_agent.process_query.assert_awaited_once()
257+
258+
259+
def test_query_with_agent_auto_uses_router():
260+
"""Test that --agent auto (default) uses the router to select agent."""
261+
runner = CliRunner()
262+
263+
from redis_sre_agent.agent.router import AgentType
264+
265+
mock_agent = MagicMock()
266+
mock_agent.process_query = AsyncMock(return_value="routed result")
267+
268+
with (
269+
patch(
270+
"redis_sre_agent.cli.query.get_knowledge_agent", return_value=mock_agent
271+
) as mock_get_knowledge,
272+
patch("redis_sre_agent.cli.query.get_sre_agent") as mock_get_sre,
273+
patch(
274+
"redis_sre_agent.cli.query.route_to_appropriate_agent",
275+
new=AsyncMock(return_value=AgentType.KNOWLEDGE_ONLY),
276+
) as mock_router,
277+
):
278+
# Default is auto, so router should be called
279+
result = runner.invoke(query, ["What is Redis?"])
280+
281+
assert result.exit_code == 0, result.output
282+
# Should show "Knowledge" without "(selected)" since it was auto-routed
283+
assert "Agent: Knowledge" in result.output
284+
assert "(selected)" not in result.output
285+
286+
# Router should be called
287+
mock_router.assert_awaited_once()
288+
289+
mock_get_knowledge.assert_called_once()
290+
mock_get_sre.assert_not_called()
291+
292+
293+
def test_query_agent_option_is_case_insensitive():
294+
"""Test that --agent option accepts different cases."""
295+
runner = CliRunner()
296+
297+
mock_agent = MagicMock()
298+
mock_agent.process_query = AsyncMock(return_value="result")
299+
300+
with (
301+
patch("redis_sre_agent.cli.query.get_knowledge_agent", return_value=mock_agent),
302+
patch("redis_sre_agent.cli.query.route_to_appropriate_agent"),
303+
):
304+
# Test uppercase
305+
result = runner.invoke(query, ["--agent", "KNOWLEDGE", "test query"])
306+
assert result.exit_code == 0, result.output
307+
308+
# Test mixed case
309+
result = runner.invoke(query, ["--agent", "Knowledge", "test query"])
310+
assert result.exit_code == 0, result.output

0 commit comments

Comments
 (0)