Skip to content

Commit 6fea510

Browse files
committed
Refactor AI model extensions and improve configuration handling
- Simplified the AddAIModel method to streamline AI service configuration. - Enhanced WithAIModel method to connect projects to AI services with better environment configuration. - Updated Azure OpenAI and Ollama configuration methods for clarity and maintainability. - Added new endpoints for chat and weather forecasts in the API service. - Implemented database migration handling and rate limiting extensions. - Updated package references to the latest versions for improved stability and features.
1 parent 62c7d6f commit 6fea510

File tree

19 files changed

+835
-746
lines changed

19 files changed

+835
-746
lines changed

src/BuildWithAspire.ApiService/BuildWithAspire.ApiService.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11-
<PackageReference Include="Aspire.Azure.AI.OpenAI" Version="9.5.1-preview.1.25502.11" />
12-
<PackageReference Include="Aspire.Azure.AI.Inference" Version="9.5.1-preview.1.25502.11" />
13-
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.5.1" />
11+
<PackageReference Include="Aspire.Azure.AI.OpenAI" Version="9.5.2-preview.1.25522.3" />
12+
<PackageReference Include="Aspire.Azure.AI.Inference" Version="9.5.2-preview.1.25522.3" />
13+
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.5.2" />
1414
<PackageReference Include="CommunityToolkit.Aspire.OllamaSharp" Version="9.8.0" />
1515
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
1616
<PackageReference Include="Azure.Identity" Version="1.17.0" />
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
using BuildWithAspire.ApiService.Data;
2+
using BuildWithAspire.ApiService.Models;
3+
using BuildWithAspire.ApiService.Services;
4+
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.EntityFrameworkCore;
6+
7+
namespace BuildWithAspire.ApiService.Endpoints;
8+
9+
/// <summary>
10+
/// Chat conversation and message endpoints.
11+
/// </summary>
12+
public static class ChatEndpoints
13+
{
14+
public static RouteGroupBuilder MapChatEndpoints(this IEndpointRouteBuilder routes)
15+
{
16+
var group = routes.MapGroup("/conversations");
17+
18+
group.MapGet("/", GetConversations)
19+
.WithName("GetConversations")
20+
.WithOpenApi();
21+
22+
group.MapGet("/{id}", GetConversation)
23+
.WithName("GetConversation")
24+
.WithOpenApi();
25+
26+
group.MapPost("/", CreateConversation)
27+
.WithName("CreateConversation")
28+
.WithOpenApi();
29+
30+
group.MapPost("/{id}/messages", SendMessage)
31+
.WithName("SendMessage")
32+
.WithOpenApi()
33+
.RequireRateLimiting("chat");
34+
35+
group.MapDelete("/{id}", DeleteConversation)
36+
.WithName("DeleteConversation")
37+
.WithOpenApi();
38+
39+
// Legacy endpoint for backward compatibility
40+
routes.MapGet("/chat", async (ChatService chatService, string message) =>
41+
await chatService.ProcessMessage(message).ConfigureAwait(false))
42+
.WithName("GetChat")
43+
.WithOpenApi()
44+
.RequireRateLimiting("chat");
45+
46+
return group;
47+
}
48+
49+
private static async Task<IResult> GetConversations(ChatDbContext db, ILogger<Program> logger)
50+
{
51+
try
52+
{
53+
var conversations = await db.Conversations
54+
.OrderByDescending(c => c.UpdatedAt)
55+
.Select(c => new
56+
{
57+
c.Id,
58+
c.Name,
59+
c.CreatedAt,
60+
c.UpdatedAt,
61+
MessageCount = c.Messages.Count()
62+
})
63+
.ToListAsync().ConfigureAwait(false);
64+
65+
return Results.Ok(conversations);
66+
}
67+
catch (Exception ex)
68+
{
69+
logger.LogError(ex, "Error retrieving conversations");
70+
return Results.Problem("Failed to retrieve conversations", statusCode: 500);
71+
}
72+
}
73+
74+
private static async Task<IResult> GetConversation(Guid id, ChatDbContext db, ILogger<Program> logger)
75+
{
76+
try
77+
{
78+
var conversation = await db.Conversations
79+
.Include(c => c.Messages.OrderBy(m => m.CreatedAt))
80+
.FirstOrDefaultAsync(c => c.Id == id).ConfigureAwait(false);
81+
82+
return conversation == null ? Results.NotFound() : Results.Ok(conversation);
83+
}
84+
catch (Exception ex)
85+
{
86+
logger.LogError(ex, "Error retrieving conversation: {ConversationId}", id);
87+
return Results.Problem("Failed to retrieve conversation", statusCode: 500);
88+
}
89+
}
90+
91+
private static async Task<IResult> CreateConversation([FromBody] CreateConversationRequest request, ChatDbContext db, ILogger<Program> logger)
92+
{
93+
try
94+
{
95+
if (string.IsNullOrWhiteSpace(request.Name))
96+
{
97+
return Results.BadRequest("Conversation name cannot be empty");
98+
}
99+
100+
var conversation = new Conversation
101+
{
102+
Id = Guid.NewGuid(),
103+
Name = request.Name,
104+
CreatedAt = DateTime.UtcNow,
105+
UpdatedAt = DateTime.UtcNow
106+
};
107+
108+
db.Conversations.Add(conversation);
109+
await db.SaveChangesAsync().ConfigureAwait(false);
110+
111+
return Results.Created($"/conversations/{conversation.Id}", conversation);
112+
}
113+
catch (Exception ex)
114+
{
115+
logger.LogError(ex, "Error creating conversation: {ConversationName}", request.Name);
116+
return Results.Problem("Failed to create conversation", statusCode: 500);
117+
}
118+
}
119+
120+
private static async Task<IResult> SendMessage(
121+
Guid id,
122+
[FromBody] SendMessageRequest request,
123+
ChatService chatService,
124+
ChatDbContext db,
125+
ILogger<Program> logger)
126+
{
127+
try
128+
{
129+
if (string.IsNullOrWhiteSpace(request.Message))
130+
{
131+
return Results.BadRequest("Message cannot be empty");
132+
}
133+
134+
var conversation = await db.Conversations
135+
.Include(c => c.Messages)
136+
.FirstOrDefaultAsync(c => c.Id == id).ConfigureAwait(false);
137+
138+
if (conversation == null)
139+
{
140+
return Results.NotFound("Conversation not found");
141+
}
142+
143+
// Add user message
144+
var userMessage = new Message
145+
{
146+
Id = Guid.NewGuid(),
147+
ConversationId = id,
148+
Role = "user",
149+
Content = request.Message,
150+
CreatedAt = DateTime.UtcNow
151+
};
152+
153+
db.Messages.Add(userMessage);
154+
conversation.UpdatedAt = DateTime.UtcNow;
155+
await db.SaveChangesAsync().ConfigureAwait(false);
156+
157+
// Get conversation history
158+
var messages = await db.Messages
159+
.Where(m => m.ConversationId == id)
160+
.OrderBy(m => m.CreatedAt)
161+
.Select(m => new ChatMessageRequest { Role = m.Role, Content = m.Content })
162+
.ToListAsync().ConfigureAwait(false);
163+
164+
// Get AI response
165+
string aiResponse;
166+
try
167+
{
168+
aiResponse = await chatService.ProcessMessagesWithHistory(messages).ConfigureAwait(false);
169+
}
170+
catch (Exception ex)
171+
{
172+
logger.LogError(ex, "AI service error for conversation {ConversationId}", id);
173+
return Results.Problem("Failed to get AI response", statusCode: 500);
174+
}
175+
176+
// Add assistant message
177+
var assistantMessage = new Message
178+
{
179+
Id = Guid.NewGuid(),
180+
ConversationId = id,
181+
Role = "assistant",
182+
Content = aiResponse,
183+
CreatedAt = DateTime.UtcNow
184+
};
185+
186+
db.Messages.Add(assistantMessage);
187+
await db.SaveChangesAsync().ConfigureAwait(false);
188+
189+
return Results.Ok(new { response = aiResponse });
190+
}
191+
catch (Exception ex)
192+
{
193+
logger.LogError(ex, "Error in SendMessage for conversation {ConversationId}", id);
194+
return Results.Problem("Internal server error", statusCode: 500);
195+
}
196+
}
197+
198+
private static async Task<IResult> DeleteConversation(Guid id, ChatDbContext db)
199+
{
200+
var conversation = await db.Conversations.FindAsync(id).ConfigureAwait(false);
201+
if (conversation == null)
202+
{
203+
return Results.NotFound();
204+
}
205+
206+
db.Conversations.Remove(conversation);
207+
await db.SaveChangesAsync().ConfigureAwait(false);
208+
209+
return Results.NoContent();
210+
}
211+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
using BuildWithAspire.ApiService.Services;
2+
using Microsoft.AspNetCore.Mvc;
3+
4+
namespace BuildWithAspire.ApiService.Endpoints;
5+
6+
/// <summary>
7+
/// MCP (Model Context Protocol) tool integration endpoints.
8+
/// </summary>
9+
public static class McpEndpoints
10+
{
11+
public static RouteGroupBuilder MapMcpEndpoints(this IEndpointRouteBuilder routes)
12+
{
13+
var group = routes.MapGroup("/mcp");
14+
15+
group.MapPost("/call/{toolName}", CallTool)
16+
.WithName("CallMcpTool")
17+
.WithOpenApi()
18+
.RequireRateLimiting("weather");
19+
20+
group.MapGet("/tools", ListTools)
21+
.WithName("ListMcpTools")
22+
.WithOpenApi();
23+
24+
group.MapGet("/health", HealthCheck)
25+
.WithName("McpHealthCheck")
26+
.WithOpenApi();
27+
28+
return group;
29+
}
30+
31+
private static async Task<IResult> CallTool(
32+
string toolName,
33+
[FromBody] Dictionary<string, object?>? parameters,
34+
IMcpClient mcpClient,
35+
ILogger<Program> logger)
36+
{
37+
try
38+
{
39+
logger.LogInformation("MCP tool call: {ToolName}", toolName);
40+
41+
var initialized = await mcpClient.InitializeAsync().ConfigureAwait(false);
42+
if (!initialized)
43+
{
44+
logger.LogError("Failed to initialize MCP client");
45+
return Results.Problem("MCP client initialization failed", statusCode: 503);
46+
}
47+
48+
var result = await mcpClient.CallToolAsync(toolName, parameters).ConfigureAwait(false);
49+
logger.LogInformation("MCP tool {ToolName} completed. IsError: {IsError}", toolName, result.IsError);
50+
51+
return result.IsError ? Results.BadRequest(result) : Results.Ok(result);
52+
}
53+
catch (Exception ex)
54+
{
55+
logger.LogError(ex, "Error calling MCP tool: {ToolName}", toolName);
56+
57+
if (ex.Message.Contains("Unknown tool", StringComparison.OrdinalIgnoreCase))
58+
{
59+
try
60+
{
61+
var availableTools = await mcpClient.ListToolsAsync().ConfigureAwait(false);
62+
var toolNames = string.Join(", ", availableTools.Select(t => t.Name));
63+
logger.LogWarning("Unknown tool '{ToolName}'. Available: {AvailableTools}", toolName, toolNames);
64+
return Results.Problem($"Unknown tool '{toolName}'. Available: {toolNames}", statusCode: 404);
65+
}
66+
catch
67+
{
68+
// Fall through to generic error
69+
}
70+
}
71+
72+
return Results.Problem($"Error calling MCP tool: {ex.Message}", statusCode: 500);
73+
}
74+
}
75+
76+
private static async Task<IResult> ListTools(IMcpClient mcpClient, ILogger<Program> logger)
77+
{
78+
try
79+
{
80+
logger.LogInformation("MCP tools list request");
81+
82+
var initialized = await mcpClient.InitializeAsync().ConfigureAwait(false);
83+
if (!initialized)
84+
{
85+
logger.LogError("Failed to initialize MCP client");
86+
return Results.Problem("MCP client initialization failed", statusCode: 503);
87+
}
88+
89+
var tools = await mcpClient.ListToolsAsync().ConfigureAwait(false);
90+
logger.LogInformation("Retrieved {ToolCount} MCP tools", tools.Length);
91+
92+
return Results.Ok(tools);
93+
}
94+
catch (Exception ex)
95+
{
96+
logger.LogError(ex, "Error listing MCP tools");
97+
return Results.Problem($"Error listing MCP tools: {ex.Message}", statusCode: 500);
98+
}
99+
}
100+
101+
private static async Task<IResult> HealthCheck(IMcpClient mcpClient, ILogger<Program> logger)
102+
{
103+
try
104+
{
105+
logger.LogInformation("MCP health check");
106+
107+
var initialized = await mcpClient.InitializeAsync().ConfigureAwait(false);
108+
if (!initialized)
109+
{
110+
logger.LogWarning("MCP client initialization failed");
111+
return Results.Problem("MCP server connection failed", statusCode: 503, title: "Service Unavailable");
112+
}
113+
114+
var tools = await mcpClient.ListToolsAsync().ConfigureAwait(false);
115+
logger.LogInformation("MCP health check passed. Tools: {ToolCount}", tools.Length);
116+
117+
return Results.Ok(new
118+
{
119+
status = "healthy",
120+
mcpServerConnected = true,
121+
toolsAvailable = tools.Length,
122+
timestamp = DateTime.UtcNow
123+
});
124+
}
125+
catch (Exception ex)
126+
{
127+
logger.LogError(ex, "MCP health check failed");
128+
return Results.Problem($"MCP health check failed: {ex.Message}", statusCode: 503, title: "Service Unavailable");
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)