version 2

This commit is contained in:
2025-12-16 16:52:40 +03:30
parent 61e86b1e96
commit 139924db94
52 changed files with 7350 additions and 321 deletions

View File

@@ -0,0 +1,358 @@
using GreenHome.AI.DeepSeek;
using GreenHome.Application;
using GreenHome.Domain;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
namespace GreenHome.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AIController : ControllerBase
{
private readonly IDeepSeekService deepSeekService;
private readonly IAIQueryService aiQueryService;
private readonly ILogger<AIController> logger;
public AIController(
IDeepSeekService deepSeekService,
IAIQueryService aiQueryService,
ILogger<AIController> logger)
{
this.deepSeekService = deepSeekService;
this.aiQueryService = aiQueryService;
this.logger = logger;
}
/// <summary>
/// Send a simple question to the AI and get a response
/// </summary>
/// <param name="request">Question request</param>
/// <returns>AI response</returns>
[HttpPost("ask")]
public async Task<IActionResult> AskQuestion([FromBody] SimpleQuestionRequest request)
{
var stopwatch = Stopwatch.StartNew();
try
{
if (string.IsNullOrWhiteSpace(request.Question))
{
return BadRequest(new { error = "Question is required" });
}
logger.LogInformation("Processing AI question: {Question}", request.Question);
// Build chat request to get token information
var messages = new List<ChatMessage>();
if (!string.IsNullOrWhiteSpace(request.SystemPrompt))
{
messages.Add(new ChatMessage { Role = "system", Content = request.SystemPrompt });
}
messages.Add(new ChatMessage { Role = "user", Content = request.Question });
var chatRequest = new ChatRequest { Messages = messages };
var chatResponse = await deepSeekService.AskAsync(chatRequest);
stopwatch.Stop();
if (chatResponse == null || chatResponse.Choices == null || !chatResponse.Choices.Any())
{
return StatusCode(500, new { error = "No response received from AI" });
}
var answer = chatResponse.Choices.FirstOrDefault()?.Message?.Content ?? "";
// Save to database
var aiQuery = new AIQuery
{
Question = request.Question,
Answer = answer,
DeviceId = request.DeviceId,
UserId = request.UserId,
Model = chatResponse.Model,
PromptTokens = chatResponse.Usage?.PromptTokens ?? 0,
CompletionTokens = chatResponse.Usage?.CompletionTokens ?? 0,
TotalTokens = chatResponse.Usage?.TotalTokens ?? 0,
ResponseTimeMs = stopwatch.ElapsedMilliseconds
};
await aiQueryService.SaveQueryAsync(aiQuery);
return Ok(new
{
question = request.Question,
answer = answer,
deviceId = request.DeviceId,
tokens = new
{
prompt = aiQuery.PromptTokens,
completion = aiQuery.CompletionTokens,
total = aiQuery.TotalTokens
},
responseTimeMs = aiQuery.ResponseTimeMs,
timestamp = DateTime.UtcNow
});
}
catch (Exception ex)
{
logger.LogError(ex, "Error processing AI question");
return StatusCode(500, new { error = "An error occurred while processing your question" });
}
}
/// <summary>
/// Send a complex chat request with multiple messages to the AI
/// </summary>
/// <param name="request">Extended chat request with deviceId</param>
/// <returns>AI chat response</returns>
[HttpPost("chat")]
public async Task<IActionResult> Chat([FromBody] ExtendedChatRequest request)
{
var stopwatch = Stopwatch.StartNew();
try
{
if (request.Messages == null || !request.Messages.Any())
{
return BadRequest(new { error = "At least one message is required" });
}
logger.LogInformation("Processing AI chat with {MessageCount} messages", request.Messages.Count);
var chatRequest = new ChatRequest
{
Messages = request.Messages,
Model = request.Model,
Temperature = request.Temperature,
MaxTokens = request.MaxTokens
};
var response = await deepSeekService.AskAsync(chatRequest);
stopwatch.Stop();
if (response == null || response.Choices == null || !response.Choices.Any())
{
return StatusCode(500, new { error = "No response received from AI" });
}
// Extract question and answer
var userMessage = request.Messages.LastOrDefault(m => m.Role == "user");
var question = userMessage?.Content ?? "Complex chat";
var answer = response.Choices.FirstOrDefault()?.Message?.Content ?? "";
// Save to database
var aiQuery = new AIQuery
{
Question = question,
Answer = answer,
DeviceId = request.DeviceId,
UserId = request.UserId,
Model = response.Model,
Temperature = request.Temperature,
PromptTokens = response.Usage?.PromptTokens ?? 0,
CompletionTokens = response.Usage?.CompletionTokens ?? 0,
TotalTokens = response.Usage?.TotalTokens ?? 0,
ResponseTimeMs = stopwatch.ElapsedMilliseconds
};
await aiQueryService.SaveQueryAsync(aiQuery);
return Ok(response);
}
catch (Exception ex)
{
logger.LogError(ex, "Error processing AI chat");
return StatusCode(500, new { error = "An error occurred while processing your chat request" });
}
}
/// <summary>
/// Get suggestions for smart home automation based on device data
/// </summary>
/// <param name="request">Device context request</param>
/// <returns>AI suggestions</returns>
[HttpPost("suggest")]
public async Task<IActionResult> GetSuggestions([FromBody] SuggestionRequest request)
{
var stopwatch = Stopwatch.StartNew();
try
{
var systemPrompt = @"شما یک مشاور هوشمند خانه هوشمند هستید. بر اساس داده‌های دستگاه‌های IoT، پیشنهادهای عملی و مفید برای بهینه‌سازی مصرف انرژی، راحتی و امنیت ارائه دهید. پاسخ را به زبان فارسی و به صورت خلاصه و کاربردی بنویسید.";
var question = $@"وضعیت فعلی دستگاه‌های خانه هوشمند:
{request.DeviceContext}
لطفاً پیشنهادات خود را برای بهبود وضعیت ارائه دهید.";
var messages = new List<ChatMessage>
{
new ChatMessage { Role = "system", Content = systemPrompt },
new ChatMessage { Role = "user", Content = question }
};
var chatRequest = new ChatRequest { Messages = messages };
var chatResponse = await deepSeekService.AskAsync(chatRequest);
stopwatch.Stop();
if (chatResponse == null || chatResponse.Choices == null || !chatResponse.Choices.Any())
{
return StatusCode(500, new { error = "No suggestions received from AI" });
}
var answer = chatResponse.Choices.FirstOrDefault()?.Message?.Content ?? "";
// Save to database
var aiQuery = new AIQuery
{
Question = question,
Answer = answer,
DeviceId = request.DeviceId,
UserId = request.UserId,
Model = chatResponse.Model,
PromptTokens = chatResponse.Usage?.PromptTokens ?? 0,
CompletionTokens = chatResponse.Usage?.CompletionTokens ?? 0,
TotalTokens = chatResponse.Usage?.TotalTokens ?? 0,
ResponseTimeMs = stopwatch.ElapsedMilliseconds
};
await aiQueryService.SaveQueryAsync(aiQuery);
return Ok(new
{
suggestions = answer,
deviceId = request.DeviceId,
tokens = new
{
prompt = aiQuery.PromptTokens,
completion = aiQuery.CompletionTokens,
total = aiQuery.TotalTokens
},
responseTimeMs = aiQuery.ResponseTimeMs,
timestamp = DateTime.UtcNow
});
}
catch (Exception ex)
{
logger.LogError(ex, "Error getting AI suggestions");
return StatusCode(500, new { error = "An error occurred while getting suggestions" });
}
}
/// <summary>
/// Get AI query history for a device
/// </summary>
[HttpGet("history/device/{deviceId}")]
public async Task<IActionResult> GetDeviceHistory(int deviceId, [FromQuery] int take = 50)
{
try
{
var queries = await aiQueryService.GetDeviceQueriesAsync(deviceId, take);
var totalTokens = await aiQueryService.GetDeviceTotalTokensAsync(deviceId);
return Ok(new
{
queries = queries.Select(q => new
{
q.Id,
q.Question,
q.Answer,
q.TotalTokens,
q.PromptTokens,
q.CompletionTokens,
q.Model,
q.ResponseTimeMs,
q.CreatedAt
}),
totalTokens
});
}
catch (Exception ex)
{
logger.LogError(ex, "Error getting device history");
return StatusCode(500, new { error = "An error occurred" });
}
}
/// <summary>
/// Get AI query statistics
/// </summary>
[HttpGet("stats")]
public async Task<IActionResult> GetStats()
{
try
{
var stats = await aiQueryService.GetStatsAsync();
return Ok(stats);
}
catch (Exception ex)
{
logger.LogError(ex, "Error getting stats");
return StatusCode(500, new { error = "An error occurred" });
}
}
}
/// <summary>
/// Simple question request model
/// </summary>
public class SimpleQuestionRequest
{
/// <summary>
/// The question to ask the AI
/// </summary>
public required string Question { get; set; }
/// <summary>
/// Optional system prompt to set context for the AI
/// </summary>
public string? SystemPrompt { get; set; }
/// <summary>
/// Optional device ID to associate with this query
/// </summary>
public int? DeviceId { get; set; }
/// <summary>
/// Optional user ID to associate with this query
/// </summary>
public int? UserId { get; set; }
}
/// <summary>
/// Extended chat request with device tracking
/// </summary>
public class ExtendedChatRequest
{
public required List<ChatMessage> Messages { get; set; }
public string Model { get; set; } = "deepseek-chat";
public double? Temperature { get; set; }
public int? MaxTokens { get; set; }
public int? DeviceId { get; set; }
public int? UserId { get; set; }
}
/// <summary>
/// Suggestion request for smart home automation
/// </summary>
public class SuggestionRequest
{
/// <summary>
/// Context about devices and their current state
/// </summary>
public required string DeviceContext { get; set; }
/// <summary>
/// Device ID for this suggestion request
/// </summary>
public int? DeviceId { get; set; }
/// <summary>
/// User ID for this suggestion request
/// </summary>
public int? UserId { get; set; }
}

View File

@@ -0,0 +1,85 @@
using Microsoft.AspNetCore.Mvc;
using GreenHome.Application;
namespace GreenHome.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AlertConditionsController : ControllerBase
{
private readonly IAlertConditionService alertConditionService;
public AlertConditionsController(IAlertConditionService alertConditionService)
{
this.alertConditionService = alertConditionService;
}
/// <summary>
/// دریافت تمام شرایط هشدار یک دستگاه
/// </summary>
[HttpGet("device/{deviceId}")]
public async Task<ActionResult<IReadOnlyList<AlertConditionDto>>> GetByDeviceId(int deviceId, CancellationToken cancellationToken)
{
var result = await alertConditionService.GetByDeviceIdAsync(deviceId, cancellationToken);
return Ok(result);
}
/// <summary>
/// دریافت یک شرط هشدار با ID
/// </summary>
[HttpGet("{id}")]
public async Task<ActionResult<AlertConditionDto>> GetById(int id, CancellationToken cancellationToken)
{
var result = await alertConditionService.GetByIdAsync(id, cancellationToken);
if (result == null)
{
return NotFound();
}
return Ok(result);
}
/// <summary>
/// ایجاد شرط هشدار جدید
/// </summary>
[HttpPost]
public async Task<ActionResult<int>> Create(CreateAlertConditionRequest request, CancellationToken cancellationToken)
{
var id = await alertConditionService.CreateAsync(request, cancellationToken);
return Ok(id);
}
/// <summary>
/// به‌روزرسانی شرط هشدار
/// </summary>
[HttpPut]
public async Task<ActionResult> Update(UpdateAlertConditionRequest request, CancellationToken cancellationToken)
{
await alertConditionService.UpdateAsync(request, cancellationToken);
return Ok();
}
/// <summary>
/// حذف شرط هشدار
/// </summary>
[HttpDelete("{id}")]
public async Task<ActionResult> Delete(int id, CancellationToken cancellationToken)
{
await alertConditionService.DeleteAsync(id, cancellationToken);
return Ok();
}
/// <summary>
/// فعال/غیرفعال کردن شرط هشدار
/// </summary>
[HttpPatch("{id}/toggle")]
public async Task<ActionResult> ToggleEnabled(int id, [FromBody] bool isEnabled, CancellationToken cancellationToken)
{
var result = await alertConditionService.ToggleEnabledAsync(id, isEnabled, cancellationToken);
if (!result)
{
return NotFound();
}
return Ok();
}
}

View File

@@ -0,0 +1,77 @@
using Microsoft.AspNetCore.Mvc;
using GreenHome.Application;
namespace GreenHome.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class DailyReportController : ControllerBase
{
private readonly IDailyReportService _dailyReportService;
private readonly ILogger<DailyReportController> _logger;
public DailyReportController(
IDailyReportService dailyReportService,
ILogger<DailyReportController> logger)
{
_dailyReportService = dailyReportService;
_logger = logger;
}
/// <summary>
/// دریافت یا ایجاد گزارش تحلیل روزانه گلخانه
/// </summary>
/// <param name="deviceId">شناسه دستگاه</param>
/// <param name="persianDate">تاریخ شمسی به فرمت yyyy/MM/dd</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>گزارش تحلیل روزانه</returns>
[HttpGet]
public async Task<ActionResult<DailyReportResponse>> GetDailyReport(
[FromQuery] int deviceId,
[FromQuery] string persianDate,
CancellationToken cancellationToken)
{
try
{
if (deviceId <= 0)
{
return BadRequest(new { error = "شناسه دستگاه نامعتبر است" });
}
if (string.IsNullOrWhiteSpace(persianDate))
{
return BadRequest(new { error = "تاریخ نباید خالی باشد" });
}
var request = new DailyReportRequest
{
DeviceId = deviceId,
PersianDate = persianDate.Trim()
};
var result = await _dailyReportService.GetOrCreateDailyReportAsync(request, cancellationToken);
_logger.LogInformation(
"گزارش روزانه برای دستگاه {DeviceId} و تاریخ {Date} با موفقیت برگشت داده شد (FromCache: {FromCache})",
deviceId, persianDate, result.FromCache);
return Ok(result);
}
catch (ArgumentException ex)
{
_logger.LogWarning(ex, "درخواست نامعتبر برای دستگاه {DeviceId} و تاریخ {Date}", deviceId, persianDate);
return BadRequest(new { error = ex.Message });
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, "خطا در پردازش گزارش روزانه برای دستگاه {DeviceId} و تاریخ {Date}", deviceId, persianDate);
return NotFound(new { error = ex.Message });
}
catch (Exception ex)
{
_logger.LogError(ex, "خطای سرور در دریافت گزارش روزانه برای دستگاه {DeviceId} و تاریخ {Date}", deviceId, persianDate);
return StatusCode(500, new { error = "خطای سرور در پردازش درخواست" });
}
}
}

View File

@@ -21,6 +21,7 @@
<ProjectReference Include="..\GreenHome.Infrastructure\GreenHome.Infrastructure.csproj" />
<ProjectReference Include="..\GreenHome.Sms.Ippanel\GreenHome.Sms.Ippanel.csproj" />
<ProjectReference Include="..\GreenHome.VoiceCall.Avanak\GreenHome.VoiceCall.Avanak.csproj" />
<ProjectReference Include="..\GreenHome.AI.DeepSeek\GreenHome.AI.DeepSeek.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,5 @@
using FluentValidation;
using GreenHome.AI.DeepSeek;
using GreenHome.Application;
using GreenHome.Infrastructure;
using GreenHome.Sms.Ippanel;
@@ -15,26 +16,35 @@ builder.Services.AddSwaggerGen();
builder.Services.AddAutoMapper(typeof(GreenHome.Application.MappingProfile));
builder.Services.AddValidatorsFromAssemblyContaining<GreenHome.Application.DeviceDtoValidator>();
// CORS for Next.js dev (adjust origins as needed)
// CORS Configuration
const string CorsPolicy = "DefaultCors";
builder.Services.AddCors(options =>
{
options.AddPolicy(CorsPolicy, policy =>
policy
.WithOrigins(
"http://green.nabaksoft.ir",
"https://green.nabaksoft.ir",
"http://gh1.nabaksoft.ir",
"https://gh1.nabaksoft.ir",
"http://localhost:3000",
"http://localhost:3000",
"http://127.0.0.1:3000",
"https://localhost:3000"
)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
);
{
if (builder.Environment.IsDevelopment())
{
// در محیط Development همه origin ها مجاز هستند
policy
.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
}
else
{
// در محیط Production فقط دامنه‌های مشخص
policy
.WithOrigins(
"http://green.nabaksoft.ir",
"https://green.nabaksoft.ir",
"http://gh1.nabaksoft.ir",
"https://gh1.nabaksoft.ir"
)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
}
});
});
builder.Services.AddDbContext<GreenHome.Infrastructure.GreenHomeDbContext>(options =>
@@ -46,6 +56,10 @@ builder.Services.AddScoped<GreenHome.Application.ITelemetryService, GreenHome.In
builder.Services.AddScoped<GreenHome.Application.IDeviceSettingsService, GreenHome.Infrastructure.DeviceSettingsService>();
builder.Services.AddScoped<GreenHome.Application.IAuthService, GreenHome.Infrastructure.AuthService>();
builder.Services.AddScoped<GreenHome.Application.IAlertService, GreenHome.Infrastructure.AlertService>();
builder.Services.AddScoped<GreenHome.Application.IAlertConditionService, GreenHome.Infrastructure.AlertConditionService>();
builder.Services.AddScoped<GreenHome.Application.ISunCalculatorService, GreenHome.Infrastructure.SunCalculatorService>();
builder.Services.AddScoped<GreenHome.Application.IAIQueryService, GreenHome.Infrastructure.AIQueryService>();
builder.Services.AddScoped<GreenHome.Application.IDailyReportService, GreenHome.Infrastructure.DailyReportService>();
// SMS Service Configuration
builder.Services.AddIppanelSms(builder.Configuration);
@@ -53,6 +67,9 @@ builder.Services.AddIppanelSms(builder.Configuration);
// Voice Call Service Configuration
builder.Services.AddAvanakVoiceCall(builder.Configuration);
// AI Service Configuration
builder.Services.AddDeepSeek(builder.Configuration);
var app = builder.Build();
// Apply pending migrations automatically
@@ -78,7 +95,11 @@ using (var scope = app.Services.CreateScope())
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// HTTPS Redirection فقط در Production
if (!app.Environment.IsDevelopment())
{
app.UseHttpsRedirection();
}
app.UseCors(CorsPolicy);

View File

@@ -6,7 +6,7 @@
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5064"
"applicationUrl": "http://0.0.0.0:5064"
},
"https": {
"commandName": "Project",
@@ -14,7 +14,7 @@
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7274;http://localhost:5064"
"applicationUrl": "https://0.0.0.0:7274;http://0.0.0.0:5064"
},
"IIS Express": {
"commandName": "IISExpress",

View File

@@ -4,5 +4,16 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"Default": "Server=87.107.108.119;TrustServerCertificate=True;Database=GreenHomeDb;User Id=sa;Password=qwER12#$110"
},
"AvanakVoiceCall": {
"Token": "A948B776B90CFD919B0EC60929714136CCB49DDB"
},
"IppanelSms": {
"BaseUrl": "https://edge.ippanel.com/v1",
"AuthorizationToken": "YTA1Zjk3N2EtNzkwOC00ZTg5LWFjZmYtZGEyZDAyNjNlZWQxM2Q2ZDVjYWE0MTA2Yzc1NDYzZDY1Y2VkMjlhMzcwNjA=",
"DefaultSender": "+983000505"
}
}
}

View File

@@ -11,5 +11,12 @@
"AuthorizationToken": "YTA1Zjk3N2EtNzkwOC00ZTg5LWFjZmYtZGEyZDAyNjNlZWQxM2Q2ZDVjYWE0MTA2Yzc1NDYzZDY1Y2VkMjlhMzcwNjA=",
"DefaultSender": "+983000505"
},
"DeepSeek": {
"BaseUrl": "https://api.deepseek.com",
"ApiKey": "sk-4470fc1a003a445e92f357dbe123e5a4",
"DefaultModel": "deepseek-chat",
"DefaultTemperature": 1.0
},
"AllowedHosts": "*"
}