Skip to content

Commit e4a92f5

Browse files
authored
Merge pull request #101 from Fr4nc3/main
working version
2 parents bf79fbb + 6a9135e commit e4a92f5

File tree

11 files changed

+978
-430
lines changed

11 files changed

+978
-430
lines changed

src/backend/.env.sample

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ 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=
16+
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
1517
APPLICATIONINSIGHTS_CONNECTION_STRING=
1618

1719

src/backend/app_config.py

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
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

Comments
 (0)