@@ -70,64 +70,81 @@ PostgreSQL 17 + pgvector · EF Core 10 · Docker Compose · xUnit v3 · GitHub A
7070
7171## Architecture
7272
73- Four layers , each depending only on the one below it. No microservices, no message bus, no CQRS —
74- the interesting part of this problem is the agent, and everything else stays out of its way.
73+ Four projects , each depending only on the one beneath it. No microservices, no message bus, no
74+ CQRS — the interesting part of this problem is the agent, and everything else stays out of its way.
7575
7676``` mermaid
7777flowchart TB
78- client([HTTP client])
78+ api["<b>OperationsCopilot.Api</b> · host<br/><br/>POST /api/chat · test console · OpenAPI · health"]
79+ agent["<b>OperationsCopilot.Agent</b> · orchestration<br/><br/>ChatCompletionAgent · the four tools<br/>tool-call filter · system prompt"]
80+ infra["<b>OperationsCopilot.Infrastructure</b> · adapters<br/><br/>EF Core · pgvector search · chunk + index<br/>model clients · provider selection"]
81+ domain["<b>OperationsCopilot.Domain</b> · core<br/><br/>entities · query and chat contracts · interfaces<br/><i>no dependencies on anything above</i>"]
7982
80- subgraph api["OperationsCopilot.Api — host"]
81- endpoint["POST /api/chat<br/>validation · problem details · OpenAPI"]
82- end
83+ api ==> agent ==> infra ==> domain
8384
84- subgraph agent["OperationsCopilot.Agent — orchestration"]
85- sk["ChatCompletionAgent<br/>FunctionChoiceBehavior.Auto"]
86- tools["4 kernel functions"]
87- filter["ToolCallTrackingFilter<br/>timing · budget · telemetry"]
88- end
85+ classDef layer fill:#f7f7f5,stroke:#9a9a90,stroke-width:1px,color:#22221e,rx:6,ry:6
86+ classDef core fill:#eef1f6,stroke:#5b7aa8,stroke-width:1px,color:#16202e,rx:6,ry:6
87+ class api,agent,infra layer
88+ class domain core
89+ ```
8990
90- subgraph infra["OperationsCopilot.Infrastructure — adapters"]
91- repo["OperationsRepository<br/>EF Core"]
92- search["PgVectorKnowledgeBaseSearch<br/>cosine <=> + HNSW"]
93- embed["IEmbeddingService<br/>Ollama · Azure · deterministic"]
94- indexer["KnowledgeBaseIndexer<br/>chunk · embed · upsert"]
95- end
91+ Arrows are * references* , not calls. The direction never reverses, which is what keeps the domain
92+ free of EF Core, Semantic Kernel and HTTP.
93+
94+ ** Why the agent sits above infrastructure.** It orchestrates adapters, so it is the higher layer.
95+ Its plugins depend only on the domain interfaces (` IOperationsRepository ` , ` IKnowledgeBaseSearch ` ),
96+ which is what keeps them testable without a database. The one reference into infrastructure exists
97+ so the agent can ask ` AiClientFactory ` for a model client — endpoints, credentials and provider
98+ choice stay on the infrastructure side, and the agent only knows how to wire a client into
99+ Semantic Kernel.
96100
97- subgraph domain["OperationsCopilot.Domain — core"]
98- entities["Entities · queries · chat contracts · interfaces"]
101+ ### What talks to what
102+
103+ The same system at run time. Here the arrows * are* calls, and the boundary worth noticing is
104+ which boxes sit outside the process.
105+
106+ ``` mermaid
107+ flowchart LR
108+ client([" client<br/>console or curl "])
109+ endpoint["POST /api/chat"]
110+ brain["Semantic Kernel agent"]
111+ chat{{"chat model<br/>Ollama · Azure OpenAI"}}
112+
113+ subgraph tools["the four tools — the model picks"]
114+ direction TB
115+ low["GetLowStockProducts"]
116+ sales["GetSalesSummary"]
117+ prod["GetProductDetails"]
118+ kb["SearchKnowledgeBase"]
99119 end
100120
101- db[("PostgreSQL 17 + pgvector<br/>products · inventory · sales · document_chunks")]
102- aoai{{"Ollama (local)<br/>or Azure OpenAI<br/>chat + embeddings"}}
103- docs[/"docs/knowledge-base/*.md"/]
104-
105- client --> endpoint --> sk
106- sk <--> tools
107- tools -.observed by.-> filter
108- sk <--> aoai
109- tools --> repo
110- tools --> search
111- search --> embed --> aoai
112- docs --> indexer --> embed
113- indexer --> db
114- repo --> db
115- search --> db
116-
117- agent -.depends on.-> infra -.depends on.-> domain
118-
119- classDef store fill:#e8f0fe,stroke:#4285f4,color:#111
120- classDef ext fill:#fff4e5,stroke:#f9a825,color:#111
121+ embed{{"embedding model<br/>Ollama · Azure · deterministic"}}
122+ db[("PostgreSQL 17 + pgvector<br/>products · inventory · sales<br/>document_chunks")]
123+
124+ client --> endpoint --> brain
125+ brain <--> chat
126+ brain --> tools
127+ low --> db
128+ sales --> db
129+ prod --> db
130+ kb --> embed
131+ kb --> db
132+
133+ classDef ext fill:#fdf1dd,stroke:#c8860d,stroke-width:1px,color:#3d2a05
134+ classDef store fill:#e7eef8,stroke:#3f6fa8,stroke-width:1px,color:#122135
135+ classDef node fill:#f7f7f5,stroke:#9a9a90,color:#22221e
136+ class chat,embed ext
121137 class db store
122- class aoai ext
138+ class client,endpoint,brain,low,sales,prod,kb node
139+ style tools fill:#fcfcfb,stroke:#c9c9c1,color:#5c5c55
123140```
124141
125- ** Why the agent sits above infrastructure. ** The agent orchestrates adapters, so it is the higher
126- layer. Its plugins depend only on the domain interfaces ( ` IOperationsRepository ` ,
127- ` IKnowledgeBaseSearch ` ), which is what keeps them testable without a database. The reference to
128- the infrastructure project exists so the agent can ask ` AiClientFactory ` for a model client —
129- endpoints, credentials and provider choice stay on the infrastructure side, and the agent only
130- knows how to wire a client into Semantic Kernel .
142+ Only two kinds of box leave the process: the models and the database. Chat and embeddings are
143+ drawn separately because they are two separate settings — one can run locally while the other
144+ runs in Azure. See [ model providers ] ( #model-providers ) .
145+
146+ Indexing is not shown here; it runs once at startup and is covered in
147+ [ the RAG pipeline ] ( #the-rag-pipeline ) .
131148
132149---
133150
0 commit comments