Skip to content

Commit 9c1285f

Browse files
authored
Merge pull request #250 from microsoft/int-agentic
Int agentic
2 parents 1c82e8d + 56f7ad9 commit 9c1285f

10 files changed

+871
-14
lines changed

FETCH_HEAD

Whitespace-only changes.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import requests
2+
3+
# Backend URL (adjust if using a different port)
4+
url = "http://localhost:7000/chat"
5+
6+
# Prepare your payload with session_id and user prompt
7+
data = {
8+
"session_id": "test_session_1",
9+
"prompt": "what was my last subscription for customer ID 103?"
10+
}
11+
12+
# Make the POST request to the backend
13+
response = requests.post(url, json=data)
14+
15+
# Print the response
16+
print("Response:", response.json())
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import os
2+
import logging
3+
from typing import Any, Dict, List, Optional
4+
from dotenv import load_dotenv
5+
6+
load_dotenv() # Load environment variables from .env file if needed
7+
8+
class BaseAgent:
9+
"""
10+
Base class for all agents.
11+
Not intended to be used directly.
12+
Handles environment variables, state store, and chat history.
13+
"""
14+
15+
def __init__(self, state_store: Dict[str, Any], session_id: str) -> None:
16+
self.azure_deployment = os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT")
17+
self.azure_openai_key = os.getenv("AZURE_OPENAI_API_KEY")
18+
self.azure_openai_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
19+
self.api_version = os.getenv("AZURE_OPENAI_API_VERSION")
20+
self.mcp_server_uri = os.getenv("MCP_SERVER_URI")
21+
self.openai_model_name = os.getenv("OPENAI_MODEL_NAME")
22+
23+
self.session_id = session_id
24+
self.state_store = state_store
25+
26+
self.chat_history: List[Dict[str, str]] = self.state_store.get(f"{session_id}_chat_history", [])
27+
self.state: Optional[Any] = self.state_store.get(session_id, None)
28+
logging.debug(f"Chat history for session {session_id}: {self.chat_history}")
29+
30+
def _setstate(self, state: Any) -> None:
31+
self.state_store[self.session_id] = state
32+
33+
def append_to_chat_history(self, messages: List[Dict[str, str]]) -> None:
34+
self.chat_history.extend(messages)
35+
self.state_store[f"{self.session_id}_chat_history"] = self.chat_history
36+
37+
async def chat_async(self, prompt: str) -> str:
38+
"""
39+
Override in child class!
40+
"""
41+
raise NotImplementedError("chat_async should be implemented in subclass.")
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import asyncio
2+
import logging
3+
import os
4+
from dotenv import load_dotenv
5+
from typing import Optional
6+
7+
from base_agent import BaseAgent
8+
from semantic_kernel import Kernel
9+
10+
from semantic_kernel.agents import ChatCompletionAgent, GroupChatOrchestration, RoundRobinGroupChatManager
11+
from semantic_kernel.agents.runtime import InProcessRuntime
12+
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
13+
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
14+
from semantic_kernel.connectors.mcp import MCPSsePlugin
15+
from semantic_kernel.contents import ChatMessageContent, AuthorRole
16+
from semantic_kernel.functions import KernelArguments
17+
18+
# Load .env
19+
load_dotenv()
20+
21+
# Logging setup
22+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
23+
logger = logging.getLogger(__name__)
24+
25+
26+
class Agent(BaseAgent):
27+
def __new__(cls, state_store: dict, session_id: str):
28+
if session_id in state_store:
29+
return state_store[session_id]
30+
instance = super().__new__(cls)
31+
state_store[session_id] = instance
32+
return instance
33+
34+
def __init__(self, state_store: dict, session_id: str) -> None:
35+
if hasattr(self, "_constructed"):
36+
return
37+
self._constructed = True
38+
super().__init__(state_store, session_id)
39+
self._orchestration: Optional[GroupChatOrchestration] = None
40+
41+
async def _setup_team(self) -> None:
42+
if getattr(self, "_initialized", False):
43+
return
44+
45+
# Shared service config
46+
service = AzureChatCompletion(
47+
api_key=self.azure_openai_key,
48+
endpoint=self.azure_openai_endpoint,
49+
api_version=self.api_version,
50+
deployment_name=self.azure_deployment,
51+
)
52+
53+
# Connect plugin
54+
self.contoso_plugin = MCPSsePlugin(
55+
name="ContosoMCP",
56+
description="Contoso MCP Plugin",
57+
url=self.mcp_server_uri,
58+
headers={"Content-Type": "application/json"},
59+
timeout=30,
60+
)
61+
await self.contoso_plugin.connect()
62+
63+
# Specialist kernel
64+
specialist_kernel = Kernel()
65+
specialist_kernel.add_service(service)
66+
specialist_kernel.add_plugin(self.contoso_plugin, plugin_name="ContosoMCP")
67+
68+
def make_agent(name, description, instructions, included_tools=[]):
69+
settings = specialist_kernel.get_prompt_execution_settings_from_service_id("default")
70+
function_choice_behavior = FunctionChoiceBehavior.Auto(
71+
filters={"included_functions": included_tools} if included_tools else None
72+
)
73+
74+
return ChatCompletionAgent(
75+
name=name,
76+
description=description,
77+
instructions=instructions,
78+
service=service,
79+
function_choice_behavior=function_choice_behavior,
80+
kernel=specialist_kernel,
81+
)
82+
83+
84+
participants = [
85+
make_agent(
86+
name="crm_billing",
87+
description="CRM & Billing Agent",
88+
instructions=(
89+
"You are the CRM & Billing Agent.\n"
90+
"- Query structured CRM / billing systems for account, subscription, "
91+
"invoice, and payment information as needed.\n"
92+
"- For each response you **MUST** cross‑reference relevant *Knowledge Base* articles on billing policies, payment "
93+
"processing, refund rules, etc., to ensure responses are accurate "
94+
"and policy‑compliant.\n"
95+
"- Reply with concise, structured information and flag any policy "
96+
"concerns you detect.\n"
97+
"Only respond with data you retrieve using your tools.\n"
98+
"DO NOT respond to anything out of your domain."
99+
),
100+
included_tools=[
101+
"ContosoMCP-get_all_customers",
102+
"ContosoMCP-get_customer_detail",
103+
"ContosoMCP-get_subscription_detail",
104+
"ContosoMCP-get_invoice_payments",
105+
"ContosoMCP-pay_invoice",
106+
"ContosoMCP-get_data_usage",
107+
"ContosoMCP-search_knowledge_base",
108+
"ContosoMCP-get_customer_orders",
109+
"ContosoMCP-update_subscription",
110+
"ContosoMCP-get_billing_summary",
111+
],
112+
),
113+
make_agent(
114+
name="product_promotions",
115+
description="Product & Promo Agent",
116+
instructions=(
117+
"You are the Product & Promotions Agent.\n"
118+
"- Retrieve promotional offers, product availability, eligibility "
119+
"criteria, and discount information from structured sources.\n"
120+
"- For each response you **MUST** cross‑reference relevant *Knowledge Base* FAQs, terms & conditions, "
121+
"and best practices.\n"
122+
"- Provide factual, up‑to‑date product/promo details."
123+
"Only respond with data you retrieve using your tools.\n"
124+
"DO NOT respond to anything out of your domain."
125+
),
126+
included_tools=[
127+
"ContosoMCP-get_all_customers",
128+
"ContosoMCP-get_customer_detail",
129+
"ContosoMCP-get_promotions",
130+
"ContosoMCP-get_eligible_promotions",
131+
"ContosoMCP-search_knowledge_base",
132+
"ContosoMCP-get_products",
133+
"ContosoMCP-get_product_detail",
134+
],
135+
),
136+
make_agent(
137+
name="security_authentication",
138+
description="Security & Authentication Agent",
139+
instructions=(
140+
"You are the Security & Authentication Agent.\n"
141+
"- Investigate authentication logs, account lockouts, and security "
142+
"incidents in structured security databases.\n"
143+
"- For each response you **MUST** cross‑reference relevant *Knowledge Base* security policies and "
144+
"lockout troubleshooting guides.\n"
145+
"- Return clear risk assessments and recommended remediation steps."
146+
"Only respond with data you retrieve using your tools.\n"
147+
"DO NOT respond to anything out of your domain."
148+
),
149+
included_tools=[
150+
"ContosoMCP-get_all_customers",
151+
"ContosoMCP-get_customer_detail",
152+
"ContosoMCP-get_security_logs",
153+
"ContosoMCP-search_knowledge_base",
154+
"ContosoMCP-unlock_account",
155+
],
156+
),
157+
make_agent(
158+
name="analysis_planning",
159+
description="Analysis & Planning Agent",
160+
instructions=(
161+
"You are the Analysis & Planning Agent (the planner/orchestrator).\n"
162+
"\n"
163+
"1. Decide if the user’s request can be satisfied directly:\n"
164+
" - If YES (e.g. greetings, very simple Q&A), answer immediately using the prefix:\n"
165+
" FINAL ANSWER: <your reply>\n"
166+
"\n"
167+
"2. Otherwise you MUST delegate atomic sub‑tasks one‑by‑one to specialists.\n"
168+
" - Output format WHEN DELEGATING (strict):\n"
169+
" <specialist_name>: <task>\n"
170+
" – No other text, no quotation marks, no ‘FINAL ANSWER’.\n"
171+
" - Delegate only one sub‑task per turn, then wait for the specialist’s reply.\n"
172+
"\n"
173+
"3. After all required information is gathered, compose ONE comprehensive response and\n"
174+
" send it to the user prefixed with:\n"
175+
" FINAL ANSWER: <your synthesized reply>\n"
176+
"\n"
177+
"4. If you need clarification from the user, ask it immediately and prefix with\n"
178+
" FINAL ANSWER: <your question>\n"
179+
"\n"
180+
"Specialist directory – choose the SINGLE best match for each sub‑task:\n"
181+
"- crm_billing – Accesses CRM & billing systems for account, subscription, invoice,\n"
182+
" payment status, refunds and policy compliance questions.\n"
183+
"- product_promotions – Provides product catalogue details, current promotions,\n"
184+
" discount eligibility rules and T&Cs from structured sources & FAQs.\n"
185+
"- security_authentication – Investigates authentication logs, account lock‑outs,\n"
186+
" security incidents; references security KBs and recommends remediation steps.\n"
187+
"\n"
188+
"STRICT RULES:\n"
189+
"- Do not emit planning commentary or bullet lists to the user.\n"
190+
"- Only ‘FINAL ANSWER’ messages or specialist delegations are allowed.\n"
191+
"- After all agents discuss, make sure you respond only relevant information asked as per user request.\n"
192+
"- Never include ‘FINAL ANSWER’ when talking to a specialist.\n"
193+
),
194+
),
195+
]
196+
197+
self._orchestration = GroupChatOrchestration(
198+
members=participants,
199+
manager=RoundRobinGroupChatManager(
200+
max_rounds=1,
201+
allow_repeat_speaker=False,
202+
initial_speaker="analysis_planning",
203+
completion_criteria=lambda message: str(message.content).lower().startswith("final answer:")
204+
)
205+
)
206+
207+
# <-- CHANGED: Create and start runtime once here***
208+
self._group_chat_runtime = InProcessRuntime()
209+
self._group_chat_runtime.start()
210+
211+
self._initialized = True
212+
213+
async def chat_async(self, prompt: str) -> str:
214+
await self._setup_team()
215+
216+
if not self._orchestration or not self._group_chat_runtime:
217+
return "Multi-agent system not initialized."
218+
219+
# <-- CHANGED: Removed unsupported 'agent_response_callback' argument from invoke call***
220+
orchestration_result = await self._orchestration.invoke(
221+
task=prompt,
222+
runtime=self._group_chat_runtime,
223+
# agent_response_callback=agent_response_callback, # Removed because not supported
224+
)
225+
226+
value = await orchestration_result.get()
227+
print(f"***** Final Result *****\n{value}")
228+
229+
# <-- CHANGED: Removed stopping runtime here - will stop later in main***
230+
# await self._group_chat_runtime.stop_when_idle()
231+
232+
self.append_to_chat_history([
233+
{"role": "user", "content": prompt},
234+
{"role": "assistant", "content": value}
235+
])
236+
237+
return value
238+
239+
# Demo runner
240+
if __name__ == "__main__":
241+
async def _demo():
242+
dummy_state: dict = {}
243+
agent = Agent(dummy_state, session_id="demo")
244+
user_question = "My customer id is 101, what is my current balance?"
245+
answer = await agent.chat_async(user_question)
246+
print("\n>>> Assistant reply:\n", answer)
247+
try:
248+
await agent.contoso_plugin.close()
249+
except Exception as exc:
250+
logger.warning(f"SSE plugin close failed: {exc}")
251+
252+
asyncio.run(_demo())

agentic_ai/agents/semantic_kernel/multi_agent/collaborative_multi_agent.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -344,17 +344,17 @@ async def chat_async(self, prompt: str) -> str:
344344

345345

346346
# --------------------------- Manual test helper --------------------------- #
347-
if __name__ == "__main__":
348-
349-
async def _demo() -> None:
350-
dummy_state: dict = {}
351-
agent = Agent(dummy_state, session_id="demo")
352-
user_question = "My customer id is 101, what is my current balance?"
353-
answer = await agent.chat_async(user_question)
354-
print("\n>>> Assistant reply:\n", answer)
355-
try:
356-
await agent.contoso_plugin.close()
357-
except Exception as exc:
358-
logger.warning(f"SSE plugin close failed: {exc}")
359-
360-
asyncio.run(_demo())
347+
# if __name__ == "__main__":
348+
349+
# async def _demo() -> None:
350+
# dummy_state: dict = {}
351+
# agent = Agent(dummy_state, session_id="demo")
352+
# user_question = "My customer id is 101, why is my internet bill so high?"
353+
# answer = await agent.chat_async(user_question)
354+
# print("\n>>> Assistant reply:\n", answer)
355+
# try:
356+
# await agent.contoso_plugin.close()
357+
# except Exception as exc:
358+
# logger.warning(f"SSE plugin close failed: {exc}")
359+
360+
# asyncio.run(_demo())

0 commit comments

Comments
 (0)