Skip to content

Commit c8a0fb8

Browse files
committed
refractory for app_config
1 parent c8959a4 commit c8a0fb8

File tree

8 files changed

+422
-236
lines changed

8 files changed

+422
-236
lines changed

src/backend/.env.sample

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ AZURE_AI_PROJECT_ENDPOINT=
1212
AZURE_AI_SUBSCRIPTION_ID=
1313
AZURE_AI_RESOURCE_GROUP=
1414
AZURE_AI_PROJECT_NAME=
15+
AZURE_AI_AGENT_PROJECT_CONNECTION_STRING=
1516
APPLICATIONINSIGHTS_CONNECTION_STRING=
1617

1718

src/backend/app_config.py

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
# app_config.py
2+
import os
3+
import logging
4+
from typing import Optional, List, Dict, Any
5+
from dotenv import load_dotenv
6+
from azure.identity import DefaultAzureCredential, ClientSecretCredential
7+
from azure.cosmos.aio import CosmosClient
8+
from azure.ai.projects.aio import AIProjectClient
9+
from semantic_kernel.kernel import Kernel
10+
from semantic_kernel.contents import ChatHistory
11+
from semantic_kernel.agents.azure_ai.azure_ai_agent import AzureAIAgent
12+
13+
# Load environment variables from .env file
14+
load_dotenv()
15+
16+
class AppConfig:
17+
"""Application configuration class that loads settings from environment variables."""
18+
19+
def __init__(self):
20+
"""Initialize the application configuration with environment variables."""
21+
# Azure authentication settings
22+
self.AZURE_TENANT_ID = self._get_optional("AZURE_TENANT_ID")
23+
self.AZURE_CLIENT_ID = self._get_optional("AZURE_CLIENT_ID")
24+
self.AZURE_CLIENT_SECRET = self._get_optional("AZURE_CLIENT_SECRET")
25+
26+
# CosmosDB settings
27+
self.COSMOSDB_ENDPOINT = self._get_optional("COSMOSDB_ENDPOINT", "https://localhost:8081")
28+
self.COSMOSDB_DATABASE = self._get_optional("COSMOSDB_DATABASE", "macae-database")
29+
self.COSMOSDB_CONTAINER = self._get_optional("COSMOSDB_CONTAINER", "macae-container")
30+
31+
# Azure OpenAI settings
32+
self.AZURE_OPENAI_DEPLOYMENT_NAME = self._get_required("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-35-turbo")
33+
self.AZURE_OPENAI_API_VERSION = self._get_required("AZURE_OPENAI_API_VERSION", "2023-12-01-preview")
34+
self.AZURE_OPENAI_ENDPOINT = self._get_required("AZURE_OPENAI_ENDPOINT", "https://api.openai.com/v1")
35+
self.AZURE_OPENAI_SCOPES = [f"{self._get_optional('AZURE_OPENAI_SCOPE', 'https://cognitiveservices.azure.com/.default')}"]
36+
37+
# Frontend settings
38+
self.FRONTEND_SITE_NAME = self._get_optional("FRONTEND_SITE_NAME", "http://127.0.0.1:3000")
39+
40+
# Azure AI settings
41+
self.AZURE_AI_SUBSCRIPTION_ID = self._get_required("AZURE_AI_SUBSCRIPTION_ID")
42+
self.AZURE_AI_RESOURCE_GROUP = self._get_required("AZURE_AI_RESOURCE_GROUP")
43+
self.AZURE_AI_PROJECT_NAME = self._get_required("AZURE_AI_PROJECT_NAME")
44+
self.AZURE_AI_AGENT_PROJECT_CONNECTION_STRING = self._get_required("AZURE_AI_AGENT_PROJECT_CONNECTION_STRING")
45+
46+
# Cached clients and resources
47+
self._azure_credentials = None
48+
self._cosmos_client = None
49+
self._cosmos_database = None
50+
self._ai_project_client = None
51+
52+
def _get_required(self, name: str, default: Optional[str] = None) -> str:
53+
"""Get a required configuration value from environment variables.
54+
55+
Args:
56+
name: The name of the environment variable
57+
default: Optional default value if not found
58+
59+
Returns:
60+
The value of the environment variable or default if provided
61+
62+
Raises:
63+
ValueError: If the environment variable is not found and no default is provided
64+
"""
65+
if name in os.environ:
66+
return os.environ[name]
67+
if default is not None:
68+
logging.warning("Environment variable %s not found, using default value", name)
69+
return default
70+
raise ValueError(f"Environment variable {name} not found and no default provided")
71+
72+
def _get_optional(self, name: str, default: str = "") -> str:
73+
"""Get an optional configuration value from environment variables.
74+
75+
Args:
76+
name: The name of the environment variable
77+
default: Default value if not found (default: "")
78+
79+
Returns:
80+
The value of the environment variable or the default value
81+
"""
82+
if name in os.environ:
83+
return os.environ[name]
84+
return default
85+
86+
def _get_bool(self, name: str) -> bool:
87+
"""Get a boolean configuration value from environment variables.
88+
89+
Args:
90+
name: The name of the environment variable
91+
92+
Returns:
93+
True if the environment variable exists and is set to 'true' or '1', False otherwise
94+
"""
95+
return name in os.environ and os.environ[name].lower() in ["true", "1"]
96+
97+
def get_azure_credentials(self):
98+
"""Get Azure credentials using DefaultAzureCredential.
99+
100+
Returns:
101+
DefaultAzureCredential instance for Azure authentication
102+
"""
103+
# Cache the credentials object
104+
if self._azure_credentials is not None:
105+
return self._azure_credentials
106+
107+
try:
108+
self._azure_credentials = DefaultAzureCredential()
109+
return self._azure_credentials
110+
except Exception as exc:
111+
logging.warning("Failed to create DefaultAzureCredential: %s", exc)
112+
return None
113+
114+
def get_cosmos_database_client(self):
115+
"""Get a Cosmos DB client for the configured database.
116+
117+
Returns:
118+
A Cosmos DB database client
119+
"""
120+
try:
121+
if self._cosmos_client is None:
122+
self._cosmos_client = CosmosClient(
123+
self.COSMOSDB_ENDPOINT, credential=self.get_azure_credentials()
124+
)
125+
126+
if self._cosmos_database is None:
127+
self._cosmos_database = self._cosmos_client.get_database_client(
128+
self.COSMOSDB_DATABASE
129+
)
130+
131+
return self._cosmos_database
132+
except Exception as exc:
133+
logging.error("Failed to create CosmosDB client: %s. CosmosDB is required for this application.", exc)
134+
raise
135+
136+
def create_kernel(self):
137+
"""Creates a new Semantic Kernel instance.
138+
139+
Returns:
140+
A new Semantic Kernel instance
141+
"""
142+
kernel = Kernel()
143+
return kernel
144+
145+
def get_ai_project_client(self):
146+
"""Create and return an AIProjectClient for Azure AI Foundry using from_connection_string.
147+
148+
Returns:
149+
An AIProjectClient instance
150+
"""
151+
if self._ai_project_client is not None:
152+
return self._ai_project_client
153+
154+
try:
155+
credential = self.get_azure_credentials()
156+
if credential is None:
157+
raise RuntimeError("Unable to acquire Azure credentials; ensure DefaultAzureCredential is configured")
158+
159+
connection_string = self.AZURE_AI_AGENT_PROJECT_CONNECTION_STRING
160+
self._ai_project_client = AIProjectClient.from_connection_string(
161+
credential=credential,
162+
conn_str=connection_string
163+
)
164+
logging.info("Successfully created AIProjectClient using connection string")
165+
return self._ai_project_client
166+
except Exception as exc:
167+
logging.error("Failed to create AIProjectClient: %s", exc)
168+
raise
169+
170+
async def create_azure_ai_agent(
171+
self,
172+
kernel: Kernel,
173+
agent_name: str,
174+
instructions: str,
175+
agent_type: str = "assistant",
176+
tools=None,
177+
tool_resources=None,
178+
response_format=None,
179+
temperature: float = 0.0
180+
):
181+
"""
182+
Creates a new Azure AI Agent with the specified name and instructions using AIProjectClient.
183+
184+
Args:
185+
kernel: The Semantic Kernel instance
186+
agent_name: The name of the agent
187+
instructions: The system message / instructions for the agent
188+
agent_type: The type of agent (defaults to "assistant")
189+
tools: Optional tool definitions for the agent
190+
tool_resources: Optional tool resources required by the tools
191+
response_format: Optional response format to control structured output
192+
temperature: The temperature setting for the agent (defaults to 0.0)
193+
194+
Returns:
195+
A new AzureAIAgent instance
196+
"""
197+
try:
198+
# Get the AIProjectClient
199+
project_client = self.get_ai_project_client()
200+
201+
# Tool handling: We need to distinguish between our SK functions and
202+
# the tool definitions needed by project_client.agents.create_agent
203+
tool_definitions = None
204+
kernel_functions = []
205+
206+
# If tools are provided and they are SK KernelFunctions, we need to handle them differently
207+
# than if they are already tool definitions expected by AIProjectClient
208+
if tools:
209+
# Check if tools are SK KernelFunctions
210+
if all(hasattr(tool, 'name') and hasattr(tool, 'invoke') for tool in tools):
211+
# Store the kernel functions to register with the agent later
212+
kernel_functions = tools
213+
# For now, we don't extract tool definitions from kernel functions
214+
# This would require additional code to convert SK functions to AI Project tool definitions
215+
logging.warning("Kernel functions provided as tools will be registered with the agent after creation")
216+
else:
217+
# Assume these are already proper tool definitions for create_agent
218+
tool_definitions = tools
219+
220+
# Create the agent using the project client
221+
logging.info("Creating agent '%s' with model '%s'", agent_name, self.AZURE_OPENAI_DEPLOYMENT_NAME)
222+
agent_definition = await project_client.agents.create_agent(
223+
model=self.AZURE_OPENAI_DEPLOYMENT_NAME,
224+
name=agent_name,
225+
instructions=instructions,
226+
tools=tool_definitions, # Only pass tool_definitions, not kernel functions
227+
tool_resources=tool_resources,
228+
temperature=temperature,
229+
response_format=response_format
230+
)
231+
232+
# Create the agent instance directly with project_client and definition
233+
agent_kwargs = {
234+
"client": project_client,
235+
"definition": agent_definition,
236+
"kernel": kernel
237+
}
238+
239+
# Special case for PlannerAgent which doesn't accept agent_name parameter
240+
if agent_name == "PlannerAgent":
241+
# Import the PlannerAgent class dynamically to avoid circular imports
242+
from kernel_agents.planner_agent import PlannerAgent
243+
244+
# Import CosmosMemoryContext dynamically to avoid circular imports
245+
from context.cosmos_memory_kernel import CosmosMemoryContext
246+
247+
# Create a memory store for the agent
248+
memory_store = CosmosMemoryContext(
249+
session_id="default",
250+
user_id="system",
251+
cosmos_container=self.COSMOSDB_CONTAINER,
252+
cosmos_endpoint=self.COSMOSDB_ENDPOINT,
253+
cosmos_database=self.COSMOSDB_DATABASE
254+
)
255+
256+
# Create PlannerAgent with the correct parameters
257+
agent = PlannerAgent(
258+
kernel=kernel,
259+
session_id="default",
260+
user_id="system",
261+
memory_store=memory_store,
262+
# PlannerAgent doesn't need agent_name
263+
)
264+
else:
265+
# For other agents, create using standard AzureAIAgent
266+
agent = AzureAIAgent(**agent_kwargs)
267+
268+
# Register the kernel functions with the agent if any were provided
269+
if kernel_functions:
270+
for function in kernel_functions:
271+
if hasattr(agent, 'add_function'):
272+
agent.add_function(function)
273+
274+
return agent
275+
except Exception as exc:
276+
logging.error("Failed to create Azure AI Agent: %s", exc)
277+
raise
278+
279+
280+
# Create a global instance of AppConfig
281+
config = AppConfig()

src/backend/app_kernel.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
# Local imports
2121
from middleware.health_check import HealthCheckMiddleware
2222
from auth.auth_utils import get_authenticated_user_details
23-
from config_kernel import Config
23+
# Replace Config with our new AppConfig
24+
from app_config import config
2425
from context.cosmos_memory_kernel import CosmosMemoryContext
2526
from models.messages_kernel import (
2627
HumanFeedback,
@@ -65,7 +66,8 @@
6566
# Initialize the FastAPI app
6667
app = FastAPI()
6768

68-
frontend_url = Config.FRONTEND_SITE_NAME
69+
# Use the frontend URL from our AppConfig instance
70+
frontend_url = config.FRONTEND_SITE_NAME
6971

7072
# Add this near the top of your app.py, after initializing the app
7173
app.add_middleware(

0 commit comments

Comments
 (0)