Skip to content

Commit 2986c69

Browse files
Merge pull request #283 from microsoft/dev
feat: New UI for the template and merging dev changes into main
2 parents b0440ae + 848fc4d commit 2986c69

File tree

176 files changed

+19561
-4171
lines changed

Some content is hidden

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

176 files changed

+19561
-4171
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ Solution overview
2222
The solution leverages Azure OpenAI Service, Azure Container Apps, Azure Cosmos DB, and Azure Container Registry to create an intelligent automation pipeline. It uses a multi-agent approach where specialized AI agents work together to plan, execute, and validate tasks based on user input.
2323

2424
### Solution architecture
25-
|![image](./docs/images/readme/macae-architecture.png)|
25+
|![image](./docs/images/readme/architecture.png)|
2626
|---|
2727

28-
28+
### Agentic architecture
29+
|![image](./docs/images/readme/agent_flow.png)|
30+
|---|
2931

3032
### How to customize
3133
If you'd like to customize the solution accelerator, here are some common areas to start:
@@ -111,7 +113,7 @@ either by deleting the resource group in the Portal or running `azd down`.
111113
Business Scenario
112114
</h2>
113115

114-
|![image](./docs/images/readme/macae-application.png)|
116+
|![image](./docs/images/readme/application.png)|
115117
|---|
116118

117119
<br/>

__azurite_db_queue__.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"filename":"c:\\src\\Multi-Agent-Custom-Automation-Engine-Solution-Accelerator\\__azurite_db_queue__.json","collections":[{"name":"$SERVICES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{},"constraints":null,"uniqueNames":["accountName"],"transforms":{},"objType":"$SERVICES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$QUEUES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"name":{"name":"name","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$QUEUES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$MESSAGES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"queueName":{"name":"queueName","dirty":false,"values":[]},"messageId":{"name":"messageId","dirty":false,"values":[]},"visibleTime":{"name":"visibleTime","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$MESSAGES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"}

__azurite_db_queue_extent__.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"filename":"c:\\src\\Multi-Agent-Custom-Automation-Engine-Solution-Accelerator\\__azurite_db_queue_extent__.json","collections":[{"name":"$EXTENTS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"id":{"name":"id","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$EXTENTS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"}

docs/images/readme/agent_flow.png

373 KB
Loading

docs/images/readme/application.png

71.3 KB
Loading
267 KB
Loading
-323 KB
Binary file not shown.
-418 KB
Binary file not shown.

src/backend/app_kernel.py

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
# Add this near the top of your app.py, after initializing the app
7171
app.add_middleware(
7272
CORSMiddleware,
73-
allow_origins=[frontend_url], # Add your frontend server URL
73+
allow_origins=[frontend_url],
7474
allow_credentials=True,
7575
allow_methods=["*"],
7676
allow_headers=["*"],
@@ -519,10 +519,12 @@ async def approve_step_endpoint(
519519
return {"status": "All steps approved"}
520520

521521

522-
@app.get("/api/plans", response_model=List[PlanWithSteps])
522+
@app.get("/api/plans")
523523
async def get_plans(
524-
request: Request, session_id: Optional[str] = Query(None)
525-
) -> List[PlanWithSteps]:
524+
request: Request,
525+
session_id: Optional[str] = Query(None),
526+
plan_id: Optional[str] = Query(None),
527+
):
526528
"""
527529
Retrieve plans for the current user.
528530
@@ -607,6 +609,24 @@ async def get_plans(
607609
plan_with_steps = PlanWithSteps(**plan.model_dump(), steps=steps)
608610
plan_with_steps.update_step_counts()
609611
return [plan_with_steps]
612+
if plan_id:
613+
plan = await memory_store.get_plan_by_plan_id(plan_id=plan_id)
614+
if not plan:
615+
track_event_if_configured(
616+
"GetPlanBySessionNotFound",
617+
{"status_code": 400, "detail": "Plan not found"},
618+
)
619+
raise HTTPException(status_code=404, detail="Plan not found")
620+
621+
# Use get_steps_by_plan to match the original implementation
622+
steps = await memory_store.get_steps_by_plan(plan_id=plan.id)
623+
messages = await memory_store.get_data_by_type_and_session_id(
624+
"agent_message", session_id=plan.session_id
625+
)
626+
627+
plan_with_steps = PlanWithSteps(**plan.model_dump(), steps=steps)
628+
plan_with_steps.update_step_counts()
629+
return [plan_with_steps, messages]
610630

611631
all_plans = await memory_store.get_all_plans()
612632
# Fetch steps for all plans concurrently
@@ -756,6 +776,74 @@ async def get_agent_messages(session_id: str, request: Request) -> List[AgentMes
756776
return agent_messages
757777

758778

779+
@app.get("/api/agent_messages_by_plan/{plan_id}", response_model=List[AgentMessage])
780+
async def get_agent_messages_by_plan(
781+
plan_id: str, request: Request
782+
) -> List[AgentMessage]:
783+
"""
784+
Retrieve agent messages for a specific session.
785+
786+
---
787+
tags:
788+
- Agent Messages
789+
parameters:
790+
- name: session_id
791+
in: path
792+
type: string
793+
required: true
794+
in: path
795+
type: string
796+
required: true
797+
description: The ID of the session to retrieve agent messages for
798+
responses:
799+
200:
800+
description: List of agent messages associated with the specified session
801+
schema:
802+
type: array
803+
items:
804+
type: object
805+
properties:
806+
id:
807+
type: string
808+
description: Unique ID of the agent message
809+
session_id:
810+
type: string
811+
description: Session ID associated with the message
812+
plan_id:
813+
type: string
814+
description: Plan ID related to the agent message
815+
content:
816+
type: string
817+
description: Content of the message
818+
source:
819+
type: string
820+
description: Source of the message (e.g., agent type)
821+
timestamp:
822+
type: string
823+
format: date-time
824+
description: Timestamp of the message
825+
step_id:
826+
type: string
827+
description: Optional step ID associated with the message
828+
400:
829+
description: Missing or invalid user information
830+
404:
831+
description: Agent messages not found
832+
"""
833+
authenticated_user = get_authenticated_user_details(request_headers=request.headers)
834+
user_id = authenticated_user["user_principal_id"]
835+
if not user_id:
836+
track_event_if_configured(
837+
"UserIdNotFound", {"status_code": 400, "detail": "no user"}
838+
)
839+
raise HTTPException(status_code=400, detail="no user")
840+
841+
# Initialize memory context
842+
kernel, memory_store = await initialize_runtime_and_context("", user_id)
843+
agent_messages = await memory_store.get_data_by_type_and_plan_id("agent_message")
844+
return agent_messages
845+
846+
759847
@app.delete("/api/messages")
760848
async def delete_all_messages(request: Request) -> Dict[str, str]:
761849
"""

src/backend/context/cosmos_memory_kernel.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,17 @@ async def get_plan_by_session(self, session_id: str) -> Optional[Plan]:
230230
plans = await self.query_items(query, parameters, Plan)
231231
return plans[0] if plans else None
232232

233+
async def get_plan_by_plan_id(self, plan_id: str) -> Optional[Plan]:
234+
"""Retrieve a plan associated with a session."""
235+
query = "SELECT * FROM c WHERE c.id=@id AND c.user_id=@user_id AND c.data_type=@data_type"
236+
parameters = [
237+
{"name": "@id", "value": plan_id},
238+
{"name": "@data_type", "value": "plan"},
239+
{"name": "@user_id", "value": self.user_id},
240+
]
241+
plans = await self.query_items(query, parameters, Plan)
242+
return plans[0] if plans else None
243+
233244
async def get_thread_by_session(self, session_id: str) -> Optional[Any]:
234245
"""Retrieve a plan associated with a session."""
235246
query = "SELECT * FROM c WHERE c.session_id=@session_id AND c.user_id=@user_id AND c.data_type=@data_type"
@@ -257,7 +268,7 @@ async def get_plan(self, plan_id: str) -> Optional[Plan]:
257268

258269
async def get_all_plans(self) -> List[Plan]:
259270
"""Retrieve all plans."""
260-
query = "SELECT * FROM c WHERE c.user_id=@user_id AND c.data_type=@data_type ORDER BY c._ts DESC OFFSET 0 LIMIT 5"
271+
query = "SELECT * FROM c WHERE c.user_id=@user_id AND c.data_type=@data_type ORDER BY c._ts DESC OFFSET 0 LIMIT 10"
261272
parameters = [
262273
{"name": "@data_type", "value": "plan"},
263274
{"name": "@user_id", "value": self.user_id},
@@ -431,6 +442,27 @@ async def get_data_by_type(self, data_type: str) -> List[BaseDataModel]:
431442
logging.exception(f"Failed to query data by type from Cosmos DB: {e}")
432443
return []
433444

445+
async def get_data_by_type_and_session_id(
446+
self, data_type: str, session_id: str
447+
) -> List[BaseDataModel]:
448+
"""Query the Cosmos DB for documents with the matching data_type, session_id and user_id."""
449+
await self.ensure_initialized()
450+
if self._container is None:
451+
return []
452+
453+
model_class = self.MODEL_CLASS_MAPPING.get(data_type, BaseDataModel)
454+
try:
455+
query = "SELECT * FROM c WHERE c.session_id=@session_id AND c.user_id=@user_id AND c.data_type=@data_type ORDER BY c._ts ASC"
456+
parameters = [
457+
{"name": "@session_id", "value": session_id},
458+
{"name": "@data_type", "value": data_type},
459+
{"name": "@user_id", "value": self.user_id},
460+
]
461+
return await self.query_items(query, parameters, model_class)
462+
except Exception as e:
463+
logging.exception(f"Failed to query data by type from Cosmos DB: {e}")
464+
return []
465+
434466
async def delete_item(self, item_id: str, partition_key: str) -> None:
435467
"""Delete an item from Cosmos DB."""
436468
await self.ensure_initialized()

0 commit comments

Comments
 (0)