Skip to content

Commit de2fbc6

Browse files
committed
Merge branch 'feature/azd-semantickernel' of https://github.com/microsoft/Multi-Agent-Custom-Automation-Engine-Solution-Accelerator into feature/azd-semantickernel
2 parents 798d627 + be81e13 commit de2fbc6

17 files changed

+4286
-54
lines changed

.devcontainer/devcontainer.json

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
{
22
"name": "Multi Agent Custom Automation Engine Solution Accelerator",
3-
"image": "mcr.microsoft.com/devcontainers/javascript-node:20-bullseye",
3+
"image": "mcr.microsoft.com/devcontainers/python:3.11-bullseye",
44
"features": {
5-
"ghcr.io/devcontainers/features/docker-in-docker:2": {
6-
},
5+
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
76
"ghcr.io/azure/azure-dev/azd:latest": {},
8-
"ghcr.io/devcontainers/features/azure-cli:1": {}
7+
"ghcr.io/devcontainers/features/node:1": {},
8+
"ghcr.io/devcontainers/features/azure-cli:1": {},
9+
"ghcr.io/jsburckhardt/devcontainer-features/uv:1": {}
910
},
1011
"customizations": {
1112
"vscode": {
@@ -18,13 +19,30 @@
1819
"ms-azuretools.vscode-bicep",
1920
"ms-azuretools.vscode-docker",
2021
"ms-vscode.js-debug",
21-
"ms-vscode.vscode-node-azure-pack"
22+
"ms-vscode.vscode-node-azure-pack",
23+
"charliermarsh.ruff",
24+
"exiasr.hadolint",
25+
"kevinrose.vsc-python-indent",
26+
"mosapride.zenkaku",
27+
"ms-python.python",
28+
"njpwerner.autodocstring",
29+
"redhat.vscode-yaml",
30+
"shardulm94.trailing-spaces",
31+
"tamasfe.even-better-toml",
32+
"yzhang.markdown-all-in-one",
33+
"ms-vscode.azure-account"
2234
]
2335
}
2436
},
25-
"forwardPorts": [3000, 3100],
26-
"remoteUser": "node",
37+
"postCreateCommand": "bash ./.devcontainer/setupEnv.sh",
38+
"containerEnv": {
39+
"DISPLAY": "dummy",
40+
"PYTHONUNBUFFERED": "True",
41+
"UV_LINK_MODE": "copy",
42+
"UV_PROJECT_ENVIRONMENT": "/home/vscode/.venv"
43+
},
44+
"remoteUser": "vscode",
2745
"hostRequirements": {
2846
"memory": "8gb"
2947
}
30-
}
48+
}

.devcontainer/setupEnv.sh

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
#!/bin/bash
22

3-
pip install --upgrade pip
3+
cd ./src/backend
4+
uv add -r requirements.txt
45

6+
cd ../frontend
7+
uv add -r requirements.txt
58

6-
(cd ./src/frontend; pip install -r requirements.txt)
9+
cd ..
710

811

9-
(cd ./src/backend; pip install -r requirements.txt)
12+
13+
14+
15+
16+
17+
# pip install --upgrade pip
18+
19+
20+
# (cd ./src/frontend; pip install -r requirements.txt)
21+
22+
23+
# (cd ./src/backend; pip install -r requirements.txt)
1024

1125

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"folders": [
3+
{
4+
"path": "."
5+
},
6+
// {
7+
// "path": "./src/frontend"
8+
// },
9+
// {
10+
// "path": "./src/backend"
11+
// }
12+
]
13+
}

src/backend/.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.11

src/backend/Dockerfile

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,31 @@
11
# Base Python image
2-
FROM python:3.11-slim
2+
FROM mcr.microsoft.com/devcontainers/python:3.11-bullseye AS base
3+
WORKDIR /app
34

5+
FROM base AS builder
6+
COPY --from=ghcr.io/astral-sh/uv:0.6.3 /uv /uvx /bin/
7+
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
8+
9+
WORKDIR /app
10+
COPY uv.lock pyproject.toml /app/
11+
12+
# Install the project's dependencies using the lockfile and settings
13+
RUN --mount=type=cache,target=/root/.cache/uv \
14+
--mount=type=bind,source=uv.lock,target=uv.lock \
15+
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
16+
uv sync --frozen --no-install-project --no-dev
417

518
# Backend app setup
6-
WORKDIR /src/backend
7-
COPY . .
19+
COPY . /app
20+
RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-dev
21+
22+
FROM base
23+
24+
COPY --from=builder /app /app
25+
COPY --from=builder /bin/uv /bin/uv
26+
27+
ENV PATH="/app/.venv/bin:$PATH"
828
# Install dependencies
9-
RUN pip install --no-cache-dir -r requirements.txt
29+
1030
EXPOSE 8000
11-
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
31+
CMD ["uv", "run", "uvicorn", "app_kernel:app", "--host", "0.0.0.0", "--port", "8000"]

src/backend/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
## Execute backend API Service
2+
```shell
3+
uv run uvicorn app_kernel:app --port 8000
4+
```

src/backend/app_kernel.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,18 +115,18 @@ async def input_task_endpoint(input_task: InputTask, request: Request):
115115
if not input_task.session_id:
116116
input_task.session_id = str(uuid.uuid4())
117117

118-
# Fix 2: Don't try to set user_id on InputTask directly since it doesn't have that field
119-
# Instead, include it in the JSON we'll pass to the planner
120-
121118
try:
122-
# Create just the planner agent instead of all agents
119+
# Create all agents instead of just the planner agent
120+
# This ensures other agents are created first and the planner has access to them
123121
kernel, memory_store = await initialize_runtime_and_context(input_task.session_id, user_id)
124-
planner_agent = await AgentFactory.create_agent(
125-
agent_type=AgentType.PLANNER,
126-
session_id=input_task.session_id,
122+
agents = await AgentFactory.create_all_agents(
123+
session_id=input_task.session_id,
127124
user_id=user_id
128125
)
129126

127+
# Get the planner agent from the created agents
128+
planner_agent = agents[AgentType.PLANNER]
129+
130130
# Convert input task to JSON for the kernel function, add user_id here
131131
input_task_data = input_task.model_dump()
132132
input_task_data["user_id"] = user_id
@@ -137,8 +137,11 @@ async def input_task_endpoint(input_task: InputTask, request: Request):
137137
KernelArguments(input_task_json=input_task_json)
138138
)
139139

140+
print(f"Result: {result}")
140141
# Get plan from memory store
141142
plan = await memory_store.get_plan_by_session(input_task.session_id)
143+
144+
print(f"Plan: {plan}")
142145

143146
if not plan or not plan.id:
144147
# If plan not found by session, try to extract plan ID from result
@@ -190,7 +193,6 @@ async def input_task_endpoint(input_task: InputTask, request: Request):
190193
raise HTTPException(status_code=400, detail="Error creating plan")
191194

192195

193-
194196
@app.post("/human_feedback")
195197
async def human_feedback_endpoint(human_feedback: HumanFeedback, request: Request):
196198

src/backend/kernel_agents/agent_factory.py

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -152,29 +152,37 @@ async def create_agent(
152152
agent_type_str = cls._agent_type_strings.get(agent_type, agent_type.value.lower())
153153
tools = await cls._load_tools_for_agent(kernel, agent_type_str)
154154

155-
# Build the agent definition (functions schema) if tools exist
155+
# Build the agent definition (functions schema)
156156
definition = None
157157
client = None
158+
158159
try:
159160
client = config.get_ai_project_client()
160161
except Exception as client_exc:
161162
logger.error(f"Error creating AIProjectClient: {client_exc}")
162-
raise
163+
if agent_type == AgentType.GROUP_CHAT_MANAGER:
164+
logger.info(f"Continuing with GroupChatManager creation despite AIProjectClient error")
165+
else:
166+
raise
167+
163168
try:
164-
if tools:
165-
# Create the agent definition using the AIProjectClient (project-based pattern)
169+
# Create the agent definition using the AIProjectClient (project-based pattern)
170+
# For GroupChatManager, create a definition with minimal configuration
171+
if client is not None:
166172
definition = await client.agents.create_agent(
167173
model=config.AZURE_OPENAI_DEPLOYMENT_NAME,
168174
name=agent_type_str,
169175
instructions=system_message,
170176
temperature=temperature,
171177
response_format=None # Add response_format if required
172178
)
179+
logger.info(f"Successfully created agent definition for {agent_type_str}")
173180
except Exception as agent_exc:
174-
logger.error(f"Error creating agent definition with AIProjectClient: {agent_exc}")
175-
raise
176-
if definition is None:
177-
raise RuntimeError("Failed to create agent definition from Azure AI Project. Check your Azure configuration, permissions, and network connectivity.")
181+
logger.error(f"Error creating agent definition with AIProjectClient for {agent_type_str}: {agent_exc}")
182+
if agent_type == AgentType.GROUP_CHAT_MANAGER:
183+
logger.info(f"Continuing with GroupChatManager creation despite definition error")
184+
else:
185+
raise
178186

179187
# Create the agent instance using the project-based pattern
180188
try:
@@ -215,7 +223,7 @@ async def create_agent(
215223
cls._agent_cache[session_id][agent_type] = agent
216224

217225
return agent
218-
226+
219227
@classmethod
220228
async def create_azure_ai_agent(
221229
cls,
@@ -272,7 +280,7 @@ async def _load_tools_for_agent(cls, kernel: Kernel, agent_type: str) -> List[Ke
272280
"""Load tools for an agent from the tools directory.
273281
274282
This tries to load tool configurations from JSON files. If that fails,
275-
it creates a simple helper function as a fallback.
283+
it returns an empty list for agents that don't need tools.
276284
277285
Args:
278286
kernel: The semantic kernel instance
@@ -286,9 +294,20 @@ async def _load_tools_for_agent(cls, kernel: Kernel, agent_type: str) -> List[Ke
286294
tools = BaseAgent.get_tools_from_config(kernel, agent_type)
287295
logger.info(f"Successfully loaded {len(tools)} tools for {agent_type}")
288296
return tools
297+
except FileNotFoundError:
298+
# No tool configuration file found - this is expected for some agents
299+
logger.info(f"No tools defined for agent type '{agent_type}'. Returning empty list.")
300+
return []
289301
except Exception as e:
290-
logger.warning(f"Failed to load tools for {agent_type}, using fallback: {e}")
302+
logger.warning(f"Error loading tools for {agent_type}: {e}")
291303

304+
# Return an empty list for agents without tools rather than attempting a fallback
305+
# Special handling for group_chat_manager which typically doesn't need tools
306+
if "group_chat_manager" in agent_type:
307+
logger.info(f"No tools needed for {agent_type}. Returning empty list.")
308+
return []
309+
310+
# For other agent types, try to create a simple fallback tool
292311
try:
293312
# Use PromptTemplateConfig to create a simple tool
294313
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
@@ -319,7 +338,7 @@ async def _load_tools_for_agent(cls, kernel: Kernel, agent_type: str) -> List[Ke
319338
return [function]
320339
except Exception as fallback_error:
321340
logger.error(f"Failed to create fallback tool for {agent_type}: {fallback_error}")
322-
# Return an empty list if everything fails
341+
# Return an empty list if everything fails - the agent can still function without tools
323342
return []
324343

325344
@classmethod
@@ -343,16 +362,56 @@ async def create_all_agents(
343362
if session_id in cls._agent_cache and len(cls._agent_cache[session_id]) == len(cls._agent_classes):
344363
return cls._agent_cache[session_id]
345364

346-
# Create each agent type
365+
# Create each agent type in two phases
366+
# First, create all agents except PlannerAgent and GroupChatManager
347367
agents = {}
348-
for agent_type in cls._agent_classes.keys():
368+
planner_agent_type = AgentType.PLANNER
369+
group_chat_manager_type = AgentType.GROUP_CHAT_MANAGER
370+
371+
# Initialize cache for this session if it doesn't exist
372+
if session_id not in cls._agent_cache:
373+
cls._agent_cache[session_id] = {}
374+
375+
# Phase 1: Create all agents except planner and group chat manager
376+
for agent_type in [at for at in cls._agent_classes.keys()
377+
if at != planner_agent_type and at != group_chat_manager_type]:
349378
agents[agent_type] = await cls.create_agent(
350379
agent_type=agent_type,
351380
session_id=session_id,
352381
user_id=user_id,
353382
temperature=temperature
354383
)
355-
384+
385+
# Create agent name to instance mapping for the planner
386+
agent_instances = {}
387+
for agent_type, agent in agents.items():
388+
agent_name = cls._agent_type_strings.get(agent_type).replace("_", "") + "Agent"
389+
agent_name = agent_name[0].upper() + agent_name[1:] # Capitalize first letter
390+
agent_instances[agent_name] = agent
391+
392+
# Log the agent instances for debugging
393+
logger.debug(f"Created {len(agent_instances)} agent instances for planner: {', '.join(agent_instances.keys())}")
394+
395+
# Phase 2: Create the planner agent with agent_instances
396+
planner_agent = await cls.create_agent(
397+
agent_type=planner_agent_type,
398+
session_id=session_id,
399+
user_id=user_id,
400+
temperature=temperature,
401+
agent_instances=agent_instances # Pass agent instances to the planner
402+
)
403+
agents[planner_agent_type] = planner_agent
404+
405+
# Phase 3: Create group chat manager with all agents including the planner
406+
group_chat_manager = await cls.create_agent(
407+
agent_type=group_chat_manager_type,
408+
session_id=session_id,
409+
user_id=user_id,
410+
temperature=temperature,
411+
available_agents=agent_instances # Pass all agents to group chat manager
412+
)
413+
agents[group_chat_manager_type] = group_chat_manager
414+
356415
return agents
357416

358417
@classmethod

0 commit comments

Comments
 (0)