Skip to content

Commit 65b5147

Browse files
authored
Merge pull request #6 from MicrosoftCloudEssentials-LearningHub/multi-agent
five-agent architecture base
2 parents 0c03a19 + c79d29a commit 65b5147

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2840
-62
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
app-logs.zip
77
LogFiles
88
deploy.log
9+
__pycache__
10+
*.log
911

1012
# .tfstate files
1113
*.tfstate

TROUBLESHOOTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ terraform apply
347347

348348
<!-- START BADGE -->
349349
<div align="center">
350-
<img src="https://img.shields.io/badge/Total%20views-1543-limegreen" alt="Total views">
351-
<p>Refresh Date: 2025-11-25</p>
350+
<img src="https://img.shields.io/badge/Total%20views-1557-limegreen" alt="Total views">
351+
<p>Refresh Date: 2025-11-28</p>
352352
</div>
353353
<!-- END BADGE -->

src/DATA_PIPELINE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ az search index show-statistics \
277277

278278
<!-- START BADGE -->
279279
<div align="center">
280-
<img src="https://img.shields.io/badge/Total%20views-1543-limegreen" alt="Total views">
281-
<p>Refresh Date: 2025-11-25</p>
280+
<img src="https://img.shields.io/badge/Total%20views-1557-limegreen" alt="Total views">
281+
<p>Refresh Date: 2025-11-28</p>
282282
</div>
283283
<!-- END BADGE -->

src/Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ COPY . .
1313
EXPOSE 8000
1414

1515
# Run the application
16-
CMD ["uvicorn", "chat_app:app", "--host", "0.0.0.0", "--port", "8000"]
16+
# Multi-agent entrypoint (falls back internally if multi disabled)
17+
CMD ["uvicorn", "chat_app_multi_agent:app", "--host", "0.0.0.0", "--port", "8000"]

src/app/agents/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Agent initialization module
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Local agent initializer stub.
2+
3+
This replaces remote Microsoft Foundry agent creation with deterministic
4+
pseudo agent IDs for environments where the Agents API is unavailable.
5+
It preserves the AGENT_ID: output pattern used by Terraform provisioner
6+
scripts so existing parsing logic continues to work.
7+
"""
8+
import os
9+
10+
def initialize_local_agent(env_var_name: str, name: str) -> str:
11+
pseudo_id = f"asst_local_{env_var_name}".replace('-', '_')
12+
# Persist to environment for current process (optional)
13+
os.environ[env_var_name] = pseudo_id
14+
print("=" * 60)
15+
print(f"Local pseudo agent ready: {name}")
16+
print(f"Environment Variable: {env_var_name}")
17+
print(f"AGENT_ID:{pseudo_id}")
18+
print("=" * 60)
19+
return pseudo_id
20+
21+
# Backwards compatibility name
22+
def initialize_agent(**kwargs): # type: ignore
23+
return initialize_local_agent(kwargs.get("env_var_name", "unknown"), kwargs.get("name", "Unnamed Agent"))

src/app/agents/agent_processor.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
"""
2+
Agent processor for handling interactions with Microsoft Foundry agents.
3+
Includes MCP (Model Context Protocol) integration for tool calling.
4+
"""
5+
import os
6+
import json
7+
from typing import List, Dict, Any
8+
try:
9+
from azure.ai.projects import AIProjectClient # type: ignore
10+
from azure.identity import DefaultAzureCredential # type: ignore
11+
_REMOTE_AVAILABLE = True
12+
except Exception:
13+
_REMOTE_AVAILABLE = False
14+
15+
16+
def create_function_tool_for_agent(agent_name: str) -> List[Dict[str, Any]]:
17+
"""
18+
Create function tools for a specific agent using MCP.
19+
20+
Args:
21+
agent_name: Name of the agent (e.g., 'interior_designer', 'inventory_agent')
22+
23+
Returns:
24+
List of function tool definitions
25+
"""
26+
# Placeholder for MCP tool integration
27+
# In production, this would connect to MCP servers to get available tools
28+
tools = []
29+
30+
# Define tools based on agent type
31+
if agent_name == "interior_designer":
32+
tools.append({
33+
"type": "function",
34+
"function": {
35+
"name": "create_image",
36+
"description": "Create or modify images based on user requirements",
37+
"parameters": {
38+
"type": "object",
39+
"properties": {
40+
"prompt": {"type": "string", "description": "Image generation prompt"},
41+
"path": {"type": "string", "description": "Path to existing image (optional)"}
42+
},
43+
"required": ["prompt"]
44+
}
45+
}
46+
})
47+
48+
elif agent_name == "inventory_agent":
49+
tools.append({
50+
"type": "function",
51+
"function": {
52+
"name": "inventory_check",
53+
"description": "Check inventory levels for products",
54+
"parameters": {
55+
"type": "object",
56+
"properties": {
57+
"product_dict": {
58+
"type": "object",
59+
"description": "Dictionary mapping product names to product IDs"
60+
}
61+
},
62+
"required": ["product_dict"]
63+
}
64+
}
65+
})
66+
67+
elif agent_name == "customer_loyalty":
68+
tools.append({
69+
"type": "function",
70+
"function": {
71+
"name": "customer_loyalty_check",
72+
"description": "Check customer loyalty status and calculate discount",
73+
"parameters": {
74+
"type": "object",
75+
"properties": {
76+
"customer_id": {"type": "string", "description": "Customer ID"}
77+
},
78+
"required": ["customer_id"]
79+
}
80+
}
81+
})
82+
83+
elif agent_name == "cora":
84+
# Cora (shopper agent) might have general query tools
85+
tools.append({
86+
"type": "function",
87+
"function": {
88+
"name": "search_products",
89+
"description": "Search for products in catalog",
90+
"parameters": {
91+
"type": "object",
92+
"properties": {
93+
"query": {"type": "string", "description": "Search query"}
94+
},
95+
"required": ["query"]
96+
}
97+
}
98+
})
99+
100+
return tools
101+
102+
103+
class AgentProcessor:
104+
"""Handles communication with Microsoft Foundry agents"""
105+
106+
def __init__(self, agent_id: str, project_endpoint: str = None):
107+
"""
108+
Initialize agent processor.
109+
110+
Args:
111+
agent_id: The agent ID from Microsoft Foundry
112+
project_endpoint: Optional project endpoint (reads from env if not provided)
113+
"""
114+
self.agent_id = agent_id
115+
self.project_endpoint = project_endpoint or os.environ.get("AZURE_AI_AGENT_ENDPOINT")
116+
117+
if not self.project_endpoint or not _REMOTE_AVAILABLE:
118+
raise ValueError("Remote agent support unavailable (endpoint or SDK missing)")
119+
self.client = AIProjectClient(endpoint=self.project_endpoint, credential=DefaultAzureCredential())
120+
121+
def run_conversation_with_text_stream(
122+
self,
123+
user_message: str,
124+
conversation_history: List[Dict[str, str]] = None,
125+
additional_context: Dict[str, Any] = None
126+
):
127+
"""
128+
Run a conversation with the agent and stream the response.
129+
130+
Args:
131+
user_message: The user's message
132+
conversation_history: Optional conversation history
133+
additional_context: Additional context to provide to the agent
134+
135+
Yields:
136+
Chunks of the agent's response
137+
"""
138+
try:
139+
# Create a thread for this conversation
140+
thread = self.client.agents.create_thread()
141+
142+
# Build the message content
143+
message_content = user_message
144+
if additional_context:
145+
message_content = f"Context: {json.dumps(additional_context)}\n\nUser: {user_message}"
146+
147+
# Add message to thread
148+
self.client.agents.create_message(
149+
thread_id=thread.id,
150+
role="user",
151+
content=message_content
152+
)
153+
154+
# Run the agent
155+
run = self.client.agents.create_and_process_run(
156+
thread_id=thread.id,
157+
assistant_id=self.agent_id
158+
)
159+
160+
# Get messages
161+
messages = self.client.agents.list_messages(thread_id=thread.id)
162+
163+
# Find the assistant's response
164+
for message in messages:
165+
if message.role == "assistant":
166+
for content in message.content:
167+
if hasattr(content, 'text'):
168+
yield content.text.value
169+
170+
# Clean up
171+
self.client.agents.delete_thread(thread.id)
172+
173+
except Exception as e:
174+
yield f"Error communicating with agent: {str(e)}"

src/app/agents/agents_config.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Domain-specific agent instruction consolidation.
2+
3+
Uses Cora (shopper) prompt as baseline and applies lightweight
4+
specialization for each additional domain.
5+
"""
6+
import os
7+
8+
PROMPTS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'prompts')
9+
10+
def _read(name: str) -> str:
11+
path = os.path.join(PROMPTS_DIR, name)
12+
if os.path.exists(path):
13+
with open(path, 'r', encoding='utf-8') as f:
14+
return f.read().strip()
15+
return ""
16+
17+
BASE_CORA = _read('ShopperAgentPrompt.txt') or "You are Cora, a helpful shopping assistant for Zava DIY."
18+
19+
AGENT_INSTRUCTIONS = {
20+
'cora': BASE_CORA,
21+
'interior_design': _read('InteriorDesignAgentPrompt.txt') or "Provide interior design guidance tied to product suggestions.",
22+
'inventory': _read('InventoryAgentPrompt.txt') or "Report mock inventory status with concise JSON if feasible.",
23+
'customer_loyalty': _read('CustomerLoyaltyAgentPrompt.txt') or "Determine a loyalty discount (default 10%).",
24+
'cart_management': _read('CartManagerAgentPrompt.txt') or "Maintain a cart list; support 'add <item>' and 'remove <item>'."
25+
}

src/app/agents/agents_state.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"cora": {
3+
"id": "asst_xPTPWSqJrKKTxhweUZMwXxNb",
4+
"hash": "ec1323afb9692d92de14373b05eb60026bb3738f5fb3303976f74d5c40536092",
5+
"status": "created"
6+
},
7+
"interior_designer": {
8+
"id": "asst_6YVlwwf9MRv91UCEZhKXYggr",
9+
"hash": "0fbee2d10b87eee5a9d0bd87a9d7af60f327c9093edd47d8e6683505db5aabba",
10+
"status": "created"
11+
},
12+
"inventory_agent": {
13+
"id": "asst_cFo2Nh5ncBmw9ZimlOkzBITv",
14+
"hash": "87deafceb6532b78ef075dd0e084909c4177286a0808cb9755c9a289f0076ba3",
15+
"status": "created"
16+
},
17+
"customer_loyalty": {
18+
"id": "asst_4yiWqVI0AyJ46yKFwpnNjRrS",
19+
"hash": "1e0ffd8c5b4dac8cd247b90966b513f89b5c1c366edd476ca164d27b86dc276c",
20+
"status": "created"
21+
},
22+
"cart_manager": {
23+
"id": "asst_QChARJsqcQrWKLRhOUt7ojOG",
24+
"hash": "bd3985311b2b5e0d4d88ae4583c5c3b36113d6ddbbee67e6d36924f34292ccab",
25+
"status": "created"
26+
}
27+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import os
2+
import sys
3+
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
4+
from azure.ai.projects import AIProjectClient
5+
from azure.identity import DefaultAzureCredential
6+
from dotenv import load_dotenv
7+
from agent_initializer import initialize_agent
8+
import os
9+
import sys
10+
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
11+
from dotenv import load_dotenv
12+
from agent_initializer import initialize_local_agent
13+
14+
load_dotenv()
15+
16+
PROMPT_TARGET = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'prompts', 'CartManagerAgentPrompt.txt')
17+
if os.path.exists(PROMPT_TARGET):
18+
with open(PROMPT_TARGET, 'r', encoding='utf-8') as f:
19+
_ = f.read()
20+
21+
initialize_local_agent(env_var_name="cart_manager", name="Cart Manager Agent")
22+
initialize_agent(

0 commit comments

Comments
 (0)