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