Skip to content

Commit f34ea95

Browse files
committed
Added reasoning search plus parameterization and tests
1 parent bbb064e commit f34ea95

File tree

14 files changed

+1791
-1732
lines changed

14 files changed

+1791
-1732
lines changed

conftest.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,6 @@
1111
agents_path = Path(__file__).parent.parent.parent / "backend" / "v3" / "magentic_agents"
1212
sys.path.insert(0, str(agents_path))
1313

14-
15-
@pytest.fixture(scope="session")
16-
def event_loop():
17-
"""Create an instance of the default event loop for the test session."""
18-
import asyncio
19-
loop = asyncio.get_event_loop_policy().new_event_loop()
20-
yield loop
21-
loop.close()
22-
23-
2414
@pytest.fixture
2515
def agent_env_vars():
2616
"""Common environment variables for agent testing."""

src/backend/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ dependencies = [
2727
"pytest-cov==5.0.0",
2828
"python-dotenv>=1.1.0",
2929
"python-multipart>=0.0.20",
30-
"semantic-kernel>=1.32.2",
30+
"semantic-kernel[azure]>=1.32.2",
3131
"uvicorn>=0.34.2",
3232
"pylint-pydantic>=0.3.5",
33+
"pexpect>=4.9.0",
3334
]

src/backend/uv.lock

Lines changed: 1352 additions & 1623 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/backend/v3/magentic_agents/foundry_agent.py

Lines changed: 3 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ def __init__(self, agent_name: str,
4848
self._search_connection = None
4949
self._bing_connection = None
5050
self.logger = logging.getLogger(__name__)
51+
# input validation
52+
if self.model_deployment_name is any(["o3", "o4-mini"]):
53+
raise ValueError("The current version of Foundry agents do not support reasoning models.")
5154

5255
async def _make_bing_tool(self) -> Optional[BingGroundingTool]:
5356
"""Create Bing search tool for web search."""
@@ -169,70 +172,3 @@ async def create_foundry_agent(agent_name:str,
169172
await agent.open()
170173
return agent
171174

172-
# Manual Test harness
173-
AGENT_NAME = "TestFoundryAgent"
174-
AGENT_DESCRIPTION = "A comprehensive research assistant with web search, Azure AI Search RAG, and MCP capabilities."
175-
AGENT_INSTRUCTIONS = (
176-
"You are an Enhanced Research Agent with multiple information sources:\n"
177-
"1. Azure AI Search for retail store and customer interaction data. Some of these are in json format, others in .csv\n"
178-
"2. Bing search for current web information and recent events\n"
179-
"3. MCP tools for specialized data access\n\n"
180-
"Search Strategy:\n"
181-
"- Use Azure AI Search first for internal/proprietary information\n"
182-
"- Use Bing search for current events, recent news, and public information\n"
183-
"- Always cite your sources and specify which search method provided the information\n"
184-
"- Provide comprehensive answers combining multiple sources when relevant\n"
185-
"- Ask for clarification only if the task is genuinely ambiguous"
186-
)
187-
MODEL_DEPLOYMENT_NAME = "gpt-4.1"
188-
async def test_agent():
189-
"""Simple chat test harness for the agent."""
190-
print("🤖 Starting agent test harness...")
191-
192-
try:
193-
# If environment variables are missing, catch exception and abort
194-
try:
195-
mcp_init = MCPConfig().from_env()
196-
bing_init = BingConfig().from_env()
197-
search_init = SearchConfig().from_env()
198-
except ValueError as ve:
199-
print(f"❌ Configuration error: {ve}")
200-
return
201-
async with FoundryAgentTemplate(agent_name=AGENT_NAME,
202-
agent_description=AGENT_DESCRIPTION,
203-
agent_instructions=AGENT_INSTRUCTIONS,
204-
model_deployment_name=MODEL_DEPLOYMENT_NAME,
205-
enable_code_interpreter=True,
206-
mcp_config=mcp_init,
207-
bing_config=bing_init,
208-
search_config=search_init) as agent:
209-
print("💬 Type 'quit' or 'exit' to stop\n")
210-
211-
while True:
212-
user_input = input("You: ").strip()
213-
214-
if user_input.lower() in ['quit', 'exit', 'q']:
215-
print("👋 Goodbye!")
216-
break
217-
218-
if not user_input:
219-
continue
220-
221-
try:
222-
print("🤖 Agent: ", end="", flush=True)
223-
async for message in agent.invoke(user_input):
224-
if hasattr(message, 'content'):
225-
print(message.content, end="", flush=True)
226-
else:
227-
print(str(message), end="", flush=True)
228-
print()
229-
230-
except Exception as e:
231-
print(f"❌ Error: {e}")
232-
233-
except Exception as e:
234-
print(f"❌ Failed to create agent: {e}")
235-
236-
237-
if __name__ == "__main__":
238-
asyncio.run(test_agent())

src/backend/v3/magentic_agents/magentic_agent_factory.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
from v3.magentic_agents.coder import create_foundry_agent as create_coder
44
from v3.magentic_agents.proxy_agent import create_proxy_agent
5-
from v3.magentic_agents.reasoner import create_custom_agent as create_reasoner
65
from v3.magentic_agents.researcher import \
76
create_foundry_agent as create_researcher
87

8+
from src.backend.v3.magentic_agents.reasoner_old import \
9+
create_custom_agent as create_reasoner
10+
911
_agent_list = []
1012

1113
async def get_agents() -> list:

src/backend/v3/magentic_agents/models/agent_models.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,19 +53,31 @@ def from_env(cls) -> "BingConfig":
5353
@dataclass(slots=True)
5454
class SearchConfig:
5555
"""Configuration for connecting to Azure AI Search."""
56-
connection_name: str = ""
57-
index_name: str = ""
56+
connection_name: str | None = None
57+
endpoint: str | None = None
58+
index_name: str | None = None
59+
embedding_endpoint: str | None = None # Azure OpenAI endpoint
60+
embedding_model: str | None = None # e.g., "text-embedding-ada-002"
61+
api_key: str | None = None # API key for Azure AI Search
5862

5963
@classmethod
6064
def from_env(cls) -> "SearchConfig":
6165
connection_name = os.getenv("AZURE_AI_SEARCH_CONNECTION_NAME")
6266
index_name = os.getenv("AZURE_AI_SEARCH_INDEX_NAME")
67+
endpoint = os.getenv("AZURE_AI_SEARCH_ENDPOINT")
68+
embedding_endpoint = os.getenv("AZURE_AI_SEARCH_EMBEDDING_ENDPOINT")
69+
embedding_model = os.getenv("AZURE_AI_SEARCH_EMBEDDING_MODEL_NAME")
70+
api_key = os.getenv("AZURE_AI_SEARCH_API_KEY")
6371

6472
# Raise exception if any required environment variable is missing
65-
if not all([connection_name, index_name]):
66-
raise ValueError(f"{cls.__name__} Missing required environment variables")
73+
if not all([connection_name, index_name, endpoint]):
74+
raise ValueError(f"{cls.__name__} Missing required Azure Search environment variables")
6775

6876
return cls(
6977
connection_name=connection_name,
7078
index_name=index_name,
79+
endpoint=endpoint,
80+
embedding_endpoint=embedding_endpoint,
81+
embedding_model=embedding_model,
82+
api_key=api_key,
7183
)

src/backend/v3/magentic_agents/proxy_agent.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@
22

33
import asyncio
44
import uuid
5+
from collections.abc import AsyncIterable
56
from typing import AsyncIterator
6-
from semantic_kernel.contents import ChatMessageContent, AuthorRole
7+
8+
from semantic_kernel.agents import ( # pylint: disable=no-name-in-module
9+
AgentResponseItem, AgentThread)
710
from semantic_kernel.agents.agent import Agent
8-
from semantic_kernel.agents import AgentThread, AgentResponseItem #pylint: disable=no-name-in-module
11+
from semantic_kernel.contents import AuthorRole, ChatMessageContent
912
from semantic_kernel.contents.chat_history import ChatHistory
13+
from semantic_kernel.contents.history_reducer.chat_history_reducer import \
14+
ChatHistoryReducer
15+
from semantic_kernel.exceptions.agent_exceptions import \
16+
AgentThreadOperationException
1017
from typing_extensions import override
11-
from collections.abc import AsyncIterable
12-
from semantic_kernel.exceptions.agent_exceptions import AgentThreadOperationException
13-
from semantic_kernel.contents.history_reducer.chat_history_reducer import ChatHistoryReducer
18+
1419

1520
class DummyAgentThread(AgentThread):
1621
"""Dummy thread implementation for proxy agent."""
@@ -184,9 +189,7 @@ async def test_proxy_agent():
184189

185190
agent = ProxyAgent()
186191
test_messages = [
187-
"What's the budget for this project?",
188-
"Which department should handle this task?",
189-
"What's the timeline for completion?"
192+
"More information needed. What is the name of the employee?"
190193
]
191194

192195
for message in test_messages:
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import logging
2+
import os
3+
4+
from azure.identity import DefaultAzureCredential as SyncDefaultAzureCredential
5+
from semantic_kernel import Kernel
6+
from semantic_kernel.agents import ChatCompletionAgent # pylint: disable=E0611
7+
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
8+
from semantic_kernel.connectors.azure_ai_search import AzureAISearchCollection
9+
from v3.magentic_agents.common.lifecycle import MCPEnabledBase
10+
from v3.magentic_agents.models.agent_models import MCPConfig, SearchConfig
11+
from v3.magentic_agents.reasoning_search import ReasoningSearch
12+
13+
14+
class ReasoningAgentTemplate(MCPEnabledBase):
15+
"""
16+
SK ChatCompletionAgent with optional MCP plugin injected as a Kernel plugin.
17+
No Azure AI Agents client is needed here. We only need a token provider for SK.
18+
"""
19+
20+
def __init__(self, agent_name: str,
21+
agent_description: str,
22+
agent_instructions: str,
23+
model_deployment_name: str,
24+
azure_openai_endpoint: str,
25+
search_config: SearchConfig | None = None,
26+
mcp_config: MCPConfig | None = None) -> None:
27+
super().__init__(mcp=mcp_config)
28+
self.agent_name = agent_name
29+
self.agent_description = agent_description
30+
self.agent_instructions = agent_instructions
31+
self._model_deployment_name = model_deployment_name
32+
self._openai_endpoint = azure_openai_endpoint
33+
self.search_config = search_config
34+
self.reasoning_search: ReasoningSearch | None = None
35+
self.logger = logging.getLogger(__name__)
36+
37+
async def _after_open(self) -> None:
38+
self.kernel = Kernel()
39+
40+
# Token provider for SK chat completion
41+
sync_cred = SyncDefaultAzureCredential()
42+
43+
def ad_token_provider() -> str:
44+
token = sync_cred.get_token("https://cognitiveservices.azure.com/.default")
45+
return token.token
46+
47+
chat = AzureChatCompletion(
48+
deployment_name=self._model_deployment_name,
49+
endpoint=self._openai_endpoint,
50+
ad_token_provider=ad_token_provider
51+
)
52+
self.kernel.add_service(chat)
53+
54+
# Initialize search capabilities
55+
if self.search_config:
56+
self.reasoning_search = ReasoningSearch(self.search_config)
57+
await self.reasoning_search.initialize(self.kernel)
58+
59+
# Inject MCP plugin into the SK kernel if available
60+
if self.mcp_plugin:
61+
try:
62+
self.kernel.add_plugin(self.mcp_plugin, plugin_name="mcp_tools")
63+
self.logger.info("Added MCP plugin")
64+
except Exception as ex:
65+
self.logger.exception(f"Could not add MCP plugin to kernel: {ex}")
66+
67+
self._agent = ChatCompletionAgent(
68+
kernel=self.kernel,
69+
name=self.agent_name,
70+
description=self.agent_description,
71+
instructions=self.agent_instructions
72+
)
73+
74+
async def invoke(self, message: str):
75+
"""Invoke the agent with a message."""
76+
if not self._agent:
77+
raise RuntimeError("Agent not initialized. Call open() first.")
78+
79+
async for response in self._agent.invoke(message):
80+
yield response
81+
82+
# Backward‑compatible factory
83+
async def create_reasoning_agent(
84+
agent_name: str,
85+
agent_description: str,
86+
agent_instructions: str,
87+
model_deployment_name: str,
88+
azure_openai_endpoint: str,
89+
search_config: SearchConfig | None = None,
90+
mcp_config: MCPConfig | None = None) -> ReasoningAgentTemplate:
91+
agent = ReasoningAgentTemplate(
92+
agent_name=agent_name,
93+
agent_description=agent_description,
94+
agent_instructions=agent_instructions,
95+
model_deployment_name=model_deployment_name,
96+
azure_openai_endpoint=azure_openai_endpoint,
97+
search_config= search_config,
98+
mcp_config=mcp_config
99+
)
100+
await agent.open()
101+
return agent
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""
2+
RAG search capabilities for ReasoningAgentTemplate using AzureAISearchCollection.
3+
Based on Semantic Kernel text search patterns.
4+
"""
5+
6+
from azure.core.credentials import AzureKeyCredential
7+
from azure.identity import DefaultAzureCredential as SyncDefaultAzureCredential
8+
from azure.search.documents import SearchClient
9+
from azure.search.documents.indexes import SearchIndexClient
10+
from semantic_kernel import Kernel
11+
from semantic_kernel.connectors.ai.open_ai import AzureTextEmbedding
12+
from semantic_kernel.connectors.azure_ai_search import (
13+
AzureAISearchCollection, AzureAISearchStore)
14+
from semantic_kernel.functions import kernel_function
15+
from v3.magentic_agents.models.agent_models import SearchConfig
16+
17+
18+
class ReasoningSearch:
19+
"""Handles Azure AI Search integration for reasoning agents."""
20+
21+
def __init__(self, search_config: SearchConfig | None = None):
22+
self.search_config = search_config
23+
self.search_client: SearchClient | None = None
24+
25+
async def initialize(self, kernel: Kernel) -> bool:
26+
"""Initialize the search collection with embeddings and add it to the kernel."""
27+
if not self.search_config or not self.search_config.endpoint or not self.search_config.index_name:
28+
print("Search configuration not available")
29+
return False
30+
31+
try:
32+
credential = SyncDefaultAzureCredential()
33+
34+
self.search_client = SearchClient(endpoint=self.search_config.endpoint,
35+
credential=AzureKeyCredential(self.search_config.api_key),
36+
index_name=self.search_config.index_name)
37+
38+
# Add this class as a plugin so the agent can call search_documents
39+
kernel.add_plugin(self, plugin_name="knowledge_search")
40+
41+
print(f"Added Azure AI Search plugin for index: {self.search_config.index_name}")
42+
return True
43+
44+
except Exception as ex:
45+
print(f"Could not initialize Azure AI Search: {ex}")
46+
return False
47+
48+
@kernel_function(
49+
name="search_documents",
50+
description="Search the knowledge base for relevant documents and information. Use this when you need to find specific information from internal documents or data.",
51+
)
52+
async def search_documents(self, query: str, limit: str = "3") -> str:
53+
"""Search function that the agent can invoke to find relevant documents."""
54+
if not self.search_client:
55+
return "Search service is not available."
56+
57+
try:
58+
limit_int = int(limit)
59+
search_results = []
60+
61+
results = self.search_client.search(
62+
search_text=query,
63+
query_type= "simple",
64+
select=["content"],
65+
top=limit_int
66+
)
67+
68+
for result in results:
69+
search_results.append(f"content: {result['content']}")
70+
71+
if not search_results:
72+
return f"No relevant documents found for query: '{query}'"
73+
74+
return search_results
75+
76+
except Exception as ex:
77+
return f"Search failed: {str(ex)}"
78+
79+
def is_available(self) -> bool:
80+
"""Check if search functionality is available."""
81+
return self.search_client is not None
82+
83+
84+
# Simple factory function
85+
async def create_reasoning_search(kernel: Kernel, search_config: SearchConfig | None) -> ReasoningSearch:
86+
"""Create and initialize a ReasoningSearch instance."""
87+
search = ReasoningSearch(search_config)
88+
await search.initialize(kernel)
89+
return search

0 commit comments

Comments
 (0)