Skip to content

Commit 6b0f06f

Browse files
authored
Merge pull request #22 from redis-applied-ai/fix/query-without-redis-instance
fix: Queries with an instance ID should pass the instance to the triage agent
2 parents fa79678 + 671cf9f commit 6b0f06f

File tree

5 files changed

+148
-6
lines changed

5 files changed

+148
-6
lines changed

.github/workflows/publish-docker.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ jobs:
5656
id: meta
5757
uses: docker/metadata-action@v5
5858
with:
59-
images: abrookins/redis-sre-agent
59+
images: redislabs/redis-sre-agent
6060
tags: |
6161
type=raw,value=${{ github.event.inputs.tag }}
6262
type=raw,value=latest,enable=${{ github.event.inputs.push_latest }}

redis_sre_agent/agent/knowledge_agent.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ async def process_query(
419419
session_id: Session identifier
420420
user_id: User identifier
421421
max_iterations: Maximum number of agent iterations
422-
context: Additional context (ignored for knowledge-only agent)
422+
context: Additional context (currently ignored for knowledge-only agent)
423423
progress_callback: Optional callback for progress updates
424424
conversation_history: Optional list of previous messages for context
425425
@@ -432,9 +432,9 @@ async def process_query(
432432
if progress_callback:
433433
self.progress_callback = progress_callback
434434

435-
# Create ToolManager with only knowledge tools (no redis_instance)
435+
# Create ToolManager with Redis instance-independent tools
436436
async with ToolManager(redis_instance=None) as tool_mgr:
437-
logger.info(f"Loaded {len(tool_mgr.get_tools())} knowledge tools")
437+
logger.info(f"Loaded {len(tool_mgr.get_tools())} usable without Redis instance details")
438438

439439
# Build workflow with tools
440440
workflow = self._build_workflow(tool_mgr)

redis_sre_agent/agent/langgraph_agent.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1911,11 +1911,11 @@ async def _retry_with_backoff(
19111911
raise last_exception
19121912

19131913

1914-
def get_sre_agent() -> SRELangGraphAgent:
1914+
def get_sre_agent(*args, **kwargs) -> SRELangGraphAgent:
19151915
"""Create a new SRE agent instance for each task to prevent cross-contamination.
19161916
19171917
Previously this was a singleton, but that caused cross-contamination between
19181918
different tasks/threads when multiple tasks ran concurrently. Each task now
19191919
gets its own isolated agent instance.
19201920
"""
1921-
return SRELangGraphAgent()
1921+
return SRELangGraphAgent(*args, **kwargs)

redis_sre_agent/cli/query.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ def query(query: str, redis_instance_id: Optional[str]):
2222
async def _query():
2323
if redis_instance_id:
2424
instance = await get_instance_by_id(redis_instance_id)
25+
if not instance:
26+
click.echo(f"❌ Instance not found: {redis_instance_id}")
27+
exit(1)
2528
else:
2629
instance = None
2730

@@ -34,11 +37,13 @@ async def _query():
3437
agent = get_knowledge_agent()
3538

3639
try:
40+
context = {"instance_id": instance.id} if instance else None
3741
response = await agent.process_query(
3842
query,
3943
session_id="cli",
4044
user_id="cli_user",
4145
max_iterations=settings.max_iterations,
46+
context=context,
4247
)
4348

4449
from rich.console import Console
@@ -49,5 +54,6 @@ async def _query():
4954
console.print(Markdown(str(response)))
5055
except Exception as e:
5156
click.echo(f"❌ Error: {e}")
57+
exit(1)
5258

5359
asyncio.run(_query())

tests/unit/cli/test_cli_query.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"""Tests for the `query` CLI command."""
2+
3+
from unittest.mock import AsyncMock, MagicMock, patch
4+
5+
from click.testing import CliRunner
6+
7+
from redis_sre_agent.cli.query import query
8+
9+
10+
def test_query_cli_help_shows_instance_option():
11+
runner = CliRunner()
12+
result = runner.invoke(query, ["--help"])
13+
14+
assert result.exit_code == 0
15+
assert "--redis-instance-id" in result.output
16+
assert "-r" in result.output
17+
18+
19+
def test_query_without_instance_uses_knowledge_agent():
20+
runner = CliRunner()
21+
22+
mock_agent = MagicMock()
23+
mock_agent.process_query = AsyncMock(return_value="ok")
24+
25+
with (
26+
patch(
27+
"redis_sre_agent.cli.query.get_knowledge_agent", return_value=mock_agent
28+
) as mock_get_knowledge,
29+
patch("redis_sre_agent.cli.query.get_sre_agent") as mock_get_sre,
30+
patch(
31+
"redis_sre_agent.cli.query.get_instance_by_id",
32+
new=AsyncMock(),
33+
) as mock_get_instance,
34+
):
35+
result = runner.invoke(query, ["What is Redis SRE?"])
36+
37+
assert result.exit_code == 0, result.output
38+
mock_get_knowledge.assert_called_once()
39+
mock_get_sre.assert_not_called()
40+
mock_get_instance.assert_not_awaited()
41+
mock_agent.process_query.assert_awaited_once()
42+
43+
44+
def test_query_with_instance_uses_sre_agent_and_passes_instance_context():
45+
runner = CliRunner()
46+
47+
class DummyInstance:
48+
def __init__(self, id: str, name: str): # noqa: A003 - keep click-style arg name
49+
self.id = id
50+
self.name = name
51+
52+
instance = DummyInstance("redis-prod-123", "Haink Production")
53+
54+
mock_sre_agent = MagicMock()
55+
mock_sre_agent.process_query = AsyncMock(return_value="ok")
56+
57+
with (
58+
patch(
59+
"redis_sre_agent.cli.query.get_instance_by_id",
60+
new=AsyncMock(return_value=instance),
61+
) as mock_get_instance,
62+
patch(
63+
"redis_sre_agent.cli.query.get_sre_agent", return_value=mock_sre_agent
64+
) as mock_get_sre,
65+
patch("redis_sre_agent.cli.query.get_knowledge_agent") as mock_get_knowledge,
66+
):
67+
# Use -r / --redis-instance-id option to select instance
68+
result = runner.invoke(
69+
query,
70+
[
71+
"Should I scale this instance yet?",
72+
"-r",
73+
instance.id,
74+
],
75+
)
76+
77+
assert result.exit_code == 0, result.output
78+
79+
# Instance lookup should happen once with the provided ID
80+
mock_get_instance.assert_awaited_once_with(instance.id)
81+
82+
# SRE agent should be used (not the knowledge agent)
83+
mock_get_sre.assert_called_once()
84+
mock_get_knowledge.assert_not_called()
85+
86+
# The agent should be called exactly once
87+
mock_sre_agent.process_query.assert_awaited_once()
88+
_, kwargs = mock_sre_agent.process_query.call_args
89+
90+
# Critical behavior: CLI must pass instance context through to the agent
91+
assert kwargs.get("context") == {"instance_id": instance.id}
92+
93+
94+
def test_query_with_unknown_instance_exits_with_error_and_skips_agents():
95+
"""If -r is provided but the instance does not exist, CLI should error and exit.
96+
97+
This directly tests the new existence-check logic in redis_sre_agent.cli.query.
98+
"""
99+
100+
runner = CliRunner()
101+
102+
mock_agent = MagicMock()
103+
mock_agent.process_query = AsyncMock(return_value="ok")
104+
105+
missing_id = "nonexistent-instance-id"
106+
107+
with (
108+
patch(
109+
"redis_sre_agent.cli.query.get_instance_by_id",
110+
new=AsyncMock(return_value=None),
111+
) as mock_get_instance,
112+
patch(
113+
"redis_sre_agent.cli.query.get_knowledge_agent", return_value=mock_agent
114+
) as mock_get_knowledge,
115+
patch("redis_sre_agent.cli.query.get_sre_agent") as mock_get_sre,
116+
):
117+
result = runner.invoke(
118+
query,
119+
[
120+
"-r",
121+
missing_id,
122+
"Check the health of this instance",
123+
],
124+
)
125+
126+
# CLI should exit with non-zero status (explicitly exit(1) in implementation)
127+
assert result.exit_code == 1
128+
assert f"Instance not found: {missing_id}" in result.output
129+
130+
# We attempted to resolve the instance ID once
131+
mock_get_instance.assert_awaited_once_with(missing_id)
132+
133+
# Since the instance doesn't exist, no agent should be initialized or invoked
134+
mock_get_knowledge.assert_not_called()
135+
mock_get_sre.assert_not_called()
136+
mock_agent.process_query.assert_not_awaited()

0 commit comments

Comments
 (0)