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 ()
0 commit comments