feat: 完成用量统计功能模块
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
|
using SqlSugar;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
|
||||||
|
|
||||||
public class MessageInputDto
|
public class MessageInputDto
|
||||||
{
|
{
|
||||||
public string Content { get; set; }
|
public string Content { get; set; }
|
||||||
public string Role { get; set; }
|
public string Role { get; set; }
|
||||||
public decimal DeductCost { get; set; }
|
|
||||||
public decimal TotalTokens { get; set; }
|
|
||||||
public string ModelId { get; set; }
|
public string ModelId { get; set; }
|
||||||
public string Remark { get; set; }
|
public string? Remark { get; set; }
|
||||||
|
|
||||||
|
public TokenUsage? TokenUsage { get; set; }
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,9 @@ using Volo.Abp.Application.Services;
|
|||||||
using Volo.Abp.Users;
|
using Volo.Abp.Users;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos;
|
using Yi.Framework.AiHub.Application.Contracts.Dtos;
|
||||||
using Yi.Framework.AiHub.Domain.Entities;
|
using Yi.Framework.AiHub.Domain.Entities;
|
||||||
|
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||||
using Yi.Framework.AiHub.Domain.Managers;
|
using Yi.Framework.AiHub.Domain.Managers;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
using Yi.Framework.Rbac.Application.Contracts.IServices;
|
using Yi.Framework.Rbac.Application.Contracts.IServices;
|
||||||
using Yi.Framework.Rbac.Domain.Shared.Dtos;
|
using Yi.Framework.Rbac.Domain.Shared.Dtos;
|
||||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
@@ -28,15 +30,17 @@ public class AiChatService : ApplicationService
|
|||||||
private readonly AiMessageManager _aiMessageManager;
|
private readonly AiMessageManager _aiMessageManager;
|
||||||
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
|
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
|
||||||
private readonly AiBlacklistManager _aiBlacklistManager;
|
private readonly AiBlacklistManager _aiBlacklistManager;
|
||||||
|
private readonly UsageStatisticsManager _usageStatisticsManager;
|
||||||
|
|
||||||
public AiChatService(IHttpContextAccessor httpContextAccessor,
|
public AiChatService(IHttpContextAccessor httpContextAccessor,
|
||||||
AiMessageManager aiMessageManager, AiBlacklistManager aiBlacklistManager,
|
AiMessageManager aiMessageManager, AiBlacklistManager aiBlacklistManager,
|
||||||
ISqlSugarRepository<AiModelEntity> aiModelRepository)
|
ISqlSugarRepository<AiModelEntity> aiModelRepository, UsageStatisticsManager usageStatisticsManager)
|
||||||
{
|
{
|
||||||
this._httpContextAccessor = httpContextAccessor;
|
this._httpContextAccessor = httpContextAccessor;
|
||||||
_aiMessageManager = aiMessageManager;
|
_aiMessageManager = aiMessageManager;
|
||||||
_aiBlacklistManager = aiBlacklistManager;
|
_aiBlacklistManager = aiBlacklistManager;
|
||||||
_aiModelRepository = aiModelRepository;
|
_aiModelRepository = aiModelRepository;
|
||||||
|
_usageStatisticsManager = usageStatisticsManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -60,21 +64,21 @@ public class AiChatService : ApplicationService
|
|||||||
public async Task<List<ModelGetListOutput>> GetModelAsync()
|
public async Task<List<ModelGetListOutput>> GetModelAsync()
|
||||||
{
|
{
|
||||||
var output = await _aiModelRepository._DbQueryable
|
var output = await _aiModelRepository._DbQueryable
|
||||||
.OrderByDescending(x=>x.OrderNum)
|
.OrderByDescending(x => x.OrderNum)
|
||||||
.Select(x => new ModelGetListOutput
|
.Select(x => new ModelGetListOutput
|
||||||
{
|
{
|
||||||
Id = x.Id,
|
Id = x.Id,
|
||||||
Category = "chat",
|
Category = "chat",
|
||||||
ModelName = x.Name,
|
ModelName = x.Name,
|
||||||
ModelDescribe = x.Description,
|
ModelDescribe = x.Description,
|
||||||
ModelPrice = 0,
|
ModelPrice = 0,
|
||||||
ModelType = "1",
|
ModelType = "1",
|
||||||
ModelShow = "0",
|
ModelShow = "0",
|
||||||
SystemPrompt = null,
|
SystemPrompt = null,
|
||||||
ApiHost = null,
|
ApiHost = null,
|
||||||
ApiKey = null,
|
ApiKey = null,
|
||||||
Remark = x.Description
|
Remark = x.Description
|
||||||
}).ToListAsync();
|
}).ToListAsync();
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,13 +132,15 @@ public class AiChatService : ApplicationService
|
|||||||
|
|
||||||
var gateWay = LazyServiceProvider.GetRequiredService<AiGateWayManager>();
|
var gateWay = LazyServiceProvider.GetRequiredService<AiGateWayManager>();
|
||||||
var completeChatResponse = gateWay.CompleteChatAsync(input.Model, history, cancellationToken);
|
var completeChatResponse = gateWay.CompleteChatAsync(input.Model, history, cancellationToken);
|
||||||
|
var tokenUsage = new TokenUsage();
|
||||||
await using var writer = new StreamWriter(response.Body, Encoding.UTF8, leaveOpen: true);
|
await using var writer = new StreamWriter(response.Body, Encoding.UTF8, leaveOpen: true);
|
||||||
|
|
||||||
|
|
||||||
//缓存队列算法
|
//缓存队列算法
|
||||||
|
|
||||||
// 创建一个队列来缓存消息
|
// 创建一个队列来缓存消息
|
||||||
var messageQueue = new ConcurrentQueue<string>();
|
var messageQueue = new ConcurrentQueue<string>();
|
||||||
|
|
||||||
|
StringBuilder backupSystemContent = new StringBuilder();
|
||||||
// 设置输出速率(例如每50毫秒输出一次)
|
// 设置输出速率(例如每50毫秒输出一次)
|
||||||
var outputInterval = TimeSpan.FromMilliseconds(100);
|
var outputInterval = TimeSpan.FromMilliseconds(100);
|
||||||
// 标记是否完成接收
|
// 标记是否完成接收
|
||||||
@@ -161,33 +167,49 @@ public class AiChatService : ApplicationService
|
|||||||
|
|
||||||
await foreach (var data in completeChatResponse)
|
await foreach (var data in completeChatResponse)
|
||||||
{
|
{
|
||||||
var model = MapToMessage(input.Model, data);
|
if (data.IsFinish)
|
||||||
|
{
|
||||||
|
tokenUsage = data.TokenUsage;
|
||||||
|
}
|
||||||
|
|
||||||
|
var model = MapToMessage(input.Model, data.Content);
|
||||||
var message = JsonConvert.SerializeObject(model, new JsonSerializerSettings
|
var message = JsonConvert.SerializeObject(model, new JsonSerializerSettings
|
||||||
{
|
{
|
||||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||||
});
|
});
|
||||||
|
backupSystemContent.Append(data.Content);
|
||||||
// 将消息加入队列而不是直接写入
|
// 将消息加入队列而不是直接写入
|
||||||
messageQueue.Enqueue($"data: {message}\n");
|
messageQueue.Enqueue($"data: {message}\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标记完成并发送结束标记
|
|
||||||
isComplete = true;
|
|
||||||
//断开连接
|
//断开连接
|
||||||
messageQueue.Enqueue("data: done\n");
|
messageQueue.Enqueue("data: done\n");
|
||||||
|
// 标记完成并发送结束标记
|
||||||
|
isComplete = true;
|
||||||
|
|
||||||
await outputTask;
|
await outputTask;
|
||||||
if (CurrentUser.IsAuthenticated && input.SessionId.HasValue)
|
if (CurrentUser.IsAuthenticated && input.SessionId.HasValue)
|
||||||
{
|
{
|
||||||
await _aiMessageManager.CreateMessageAsync(CurrentUser.GetId(), input.SessionId.Value, new MessageInputDto
|
await _aiMessageManager.CreateUserMessageAsync(CurrentUser.GetId(), input.SessionId.Value,
|
||||||
{
|
new MessageInputDto
|
||||||
Content = input.Messages.LastOrDefault().Content,
|
{
|
||||||
Role = input.Messages.LastOrDefault().Role,
|
Content = input.Messages.LastOrDefault()
|
||||||
DeductCost = 0,
|
.Content,
|
||||||
TotalTokens = 0,
|
ModelId = input.Model,
|
||||||
ModelId = input.Model,
|
TokenUsage = tokenUsage,
|
||||||
Remark = null
|
});
|
||||||
});
|
|
||||||
|
await _aiMessageManager.CreateSystemMessageAsync(CurrentUser.GetId(), input.SessionId.Value,
|
||||||
|
new MessageInputDto
|
||||||
|
{
|
||||||
|
Content = backupSystemContent.ToString(),
|
||||||
|
ModelId = input.Model,
|
||||||
|
TokenUsage = tokenUsage
|
||||||
|
});
|
||||||
|
|
||||||
|
await _usageStatisticsManager.SetUsageAsync(CurrentUser.GetId(), input.Model, tokenUsage.InputTokenCount,
|
||||||
|
tokenUsage.OutputTokenCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,9 +271,9 @@ public class AiChatService : ApplicationService
|
|||||||
SystemFingerprint = "",
|
SystemFingerprint = "",
|
||||||
Usage = new Usage
|
Usage = new Usage
|
||||||
{
|
{
|
||||||
PromptTokens = 75,
|
PromptTokens = 0,
|
||||||
CompletionTokens = 25,
|
CompletionTokens = 0,
|
||||||
TotalTokens = 100,
|
TotalTokens = 0,
|
||||||
PromptTokensDetails = new()
|
PromptTokensDetails = new()
|
||||||
{
|
{
|
||||||
AudioTokens = 0,
|
AudioTokens = 0,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Volo.Abp.Application.Services;
|
|||||||
using Volo.Abp.Users;
|
using Volo.Abp.Users;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos;
|
using Yi.Framework.AiHub.Application.Contracts.Dtos;
|
||||||
using Yi.Framework.AiHub.Domain.Entities;
|
using Yi.Framework.AiHub.Domain.Entities;
|
||||||
|
using Yi.Framework.AiHub.Domain.Entities.Chat;
|
||||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Services;
|
namespace Yi.Framework.AiHub.Application.Services;
|
||||||
@@ -34,7 +35,7 @@ public class MessageService : ApplicationService
|
|||||||
var entities = await _repository._DbQueryable
|
var entities = await _repository._DbQueryable
|
||||||
.Where(x => x.SessionId == input.SessionId)
|
.Where(x => x.SessionId == input.SessionId)
|
||||||
.Where(x=>x.UserId == userId)
|
.Where(x=>x.UserId == userId)
|
||||||
.OrderByDescending(x => x.Id)
|
.OrderBy(x => x.Id)
|
||||||
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
|
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
|
||||||
return new PagedResultDto<MessageDto>(total, entities.Adapt<List<MessageDto>>());
|
return new PagedResultDto<MessageDto>(total, entities.Adapt<List<MessageDto>>());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using Volo.Abp.Domain.Repositories;
|
|||||||
using Volo.Abp.Users;
|
using Volo.Abp.Users;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos;
|
using Yi.Framework.AiHub.Application.Contracts.Dtos;
|
||||||
using Yi.Framework.AiHub.Domain.Entities;
|
using Yi.Framework.AiHub.Domain.Entities;
|
||||||
|
using Yi.Framework.AiHub.Domain.Entities.Chat;
|
||||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Services;
|
namespace Yi.Framework.AiHub.Application.Services;
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
|
|
||||||
|
public class CompleteChatResponse
|
||||||
|
{
|
||||||
|
public TokenUsage? TokenUsage { get; set; }
|
||||||
|
public bool IsFinish { get; set; }
|
||||||
|
public string? Content { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
|
|
||||||
|
public class TokenUsage
|
||||||
|
{
|
||||||
|
public int OutputTokenCount { get; set; }
|
||||||
|
|
||||||
|
public int InputTokenCount { get; set; }
|
||||||
|
|
||||||
|
public int TotalTokenCount { get; set; }
|
||||||
|
}
|
||||||
@@ -5,6 +5,6 @@ namespace Yi.Framework.AiHub.Domain.AiChat;
|
|||||||
|
|
||||||
public interface IChatService
|
public interface IChatService
|
||||||
{
|
{
|
||||||
public IAsyncEnumerable<string> CompleteChatAsync(AiModelDescribe aiModelDescribe, List<ChatMessage> messages,
|
public IAsyncEnumerable<CompleteChatResponse> CompleteChatAsync(AiModelDescribe aiModelDescribe, List<ChatMessage> messages,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,8 @@ public class AzureChatService : IChatService
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<string> CompleteChatAsync(AiModelDescribe aiModelDescribe, List<ChatMessage> messages,
|
public async IAsyncEnumerable<CompleteChatResponse> CompleteChatAsync(AiModelDescribe aiModelDescribe,
|
||||||
|
List<ChatMessage> messages,
|
||||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var endpoint = new Uri(aiModelDescribe.Endpoint);
|
var endpoint = new Uri(aiModelDescribe.Endpoint);
|
||||||
@@ -32,9 +33,28 @@ public class AzureChatService : IChatService
|
|||||||
|
|
||||||
await foreach (StreamingChatCompletionUpdate update in response)
|
await foreach (StreamingChatCompletionUpdate update in response)
|
||||||
{
|
{
|
||||||
|
var result = new CompleteChatResponse();
|
||||||
|
var isFinish = update.Usage?.OutputTokenCount is not null;
|
||||||
|
if (isFinish)
|
||||||
|
{
|
||||||
|
result.IsFinish = true;
|
||||||
|
result.TokenUsage = new TokenUsage
|
||||||
|
{
|
||||||
|
OutputTokenCount = update.Usage.OutputTokenCount,
|
||||||
|
InputTokenCount = update.Usage.InputTokenCount,
|
||||||
|
TotalTokenCount = update.Usage.TotalTokenCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
foreach (ChatMessageContentPart updatePart in update.ContentUpdate)
|
foreach (ChatMessageContentPart updatePart in update.ContentUpdate)
|
||||||
{
|
{
|
||||||
yield return updatePart.Text;
|
result.Content = updatePart.Text;
|
||||||
|
yield return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFinish)
|
||||||
|
{
|
||||||
|
yield return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ public class AzureRestChatService : IChatService
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<string> CompleteChatAsync(AiModelDescribe aiModelDescribe, List<ChatMessage> messages,
|
public async IAsyncEnumerable<CompleteChatResponse> CompleteChatAsync(AiModelDescribe aiModelDescribe,
|
||||||
|
List<ChatMessage> messages,
|
||||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// 设置API URL
|
// 设置API URL
|
||||||
@@ -61,51 +62,65 @@ public class AzureRestChatService : IChatService
|
|||||||
var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||||
// 从流中读取数据并输出到控制台
|
// 从流中读取数据并输出到控制台
|
||||||
using var streamReader = new StreamReader(responseStream);
|
using var streamReader = new StreamReader(responseStream);
|
||||||
string line;
|
while (await streamReader.ReadLineAsync(cancellationToken) is { } line)
|
||||||
while ((line = await streamReader.ReadLineAsync(cancellationToken)) != null)
|
|
||||||
{
|
{
|
||||||
var result = GetContent(line);
|
var result = new CompleteChatResponse();
|
||||||
if (result is not null)
|
try
|
||||||
{
|
{
|
||||||
yield return result;
|
var jsonObj = MapToJObject(line);
|
||||||
|
var content = GetContent(jsonObj);
|
||||||
|
var tokenUsage = GetTokenUsage(jsonObj);
|
||||||
|
result= new CompleteChatResponse
|
||||||
|
{
|
||||||
|
TokenUsage = tokenUsage,
|
||||||
|
IsFinish = tokenUsage is not null,
|
||||||
|
Content = content
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine("解析失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string? GetContent(string line)
|
private JObject? MapToJObject(string line)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(line))
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
return null;
|
return null;
|
||||||
string prefix = "data: ";
|
string prefix = "data: ";
|
||||||
line = line.Substring(prefix.Length);
|
line = line.Substring(prefix.Length);
|
||||||
|
return JObject.Parse(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetContent(JObject? jsonObj)
|
||||||
|
{
|
||||||
|
var contentToken = jsonObj.SelectToken("choices[0].delta.content");
|
||||||
|
if (contentToken != null && contentToken.Type != JTokenType.Null)
|
||||||
|
{
|
||||||
|
return contentToken.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
try
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TokenUsage? GetTokenUsage(JObject? jsonObj)
|
||||||
|
{
|
||||||
|
var usage = jsonObj.SelectToken("usage");
|
||||||
|
if (usage is not null && usage.Type != JTokenType.Null)
|
||||||
{
|
{
|
||||||
// 解析为JObject
|
var result = new TokenUsage()
|
||||||
var jsonObj = JObject.Parse(line);
|
{
|
||||||
var content = jsonObj["choices"][0]["delta"]["content"].ToString();
|
OutputTokenCount = usage["completion_tokens"].ToObject<int>(),
|
||||||
return content;
|
InputTokenCount = usage["prompt_tokens"].ToObject<int>(),
|
||||||
// // 判断choices是否存在且是数组,并且有元素
|
TotalTokenCount = usage["total_tokens"].ToObject<int>()
|
||||||
// if (jsonObj.TryGetValue("choices", out var choicesToken) && choicesToken is JArray choicesArray &&
|
};
|
||||||
// choicesArray.Count > 0)
|
|
||||||
// {
|
return result;
|
||||||
// var firstChoice = choicesArray[0] as JObject;
|
|
||||||
// // 判断delta字段是否存在
|
|
||||||
// if (firstChoice.TryGetValue("delta", out var deltaToken))
|
|
||||||
// {
|
|
||||||
// // 获取content字段
|
|
||||||
// if (deltaToken.Type == JTokenType.Object && ((JObject)deltaToken).TryGetValue("content", out var contentToken))
|
|
||||||
// {
|
|
||||||
// return contentToken.ToString();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// 解析失败
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
using SqlSugar;
|
using Mapster;
|
||||||
|
using SqlSugar;
|
||||||
using Volo.Abp.Domain.Entities.Auditing;
|
using Volo.Abp.Domain.Entities.Auditing;
|
||||||
|
using Yi.Framework.AiHub.Domain.Entities.ValueObjects;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.Entities;
|
namespace Yi.Framework.AiHub.Domain.Entities.Chat;
|
||||||
|
|
||||||
[SugarTable("Ai_Message")]
|
[SugarTable("Ai_Message")]
|
||||||
[SugarIndex($"index_{{table}}_{nameof(UserId)}_{nameof(SessionId)}",
|
[SugarIndex($"index_{{table}}_{nameof(UserId)}_{nameof(SessionId)}",
|
||||||
@@ -14,21 +17,32 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot<Guid>
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public MessageAggregateRoot(Guid userId, Guid sessionId, string content, string role, string modelId)
|
public MessageAggregateRoot(Guid userId, Guid sessionId, string content, string role, string modelId,
|
||||||
|
TokenUsage? tokenUsage)
|
||||||
{
|
{
|
||||||
UserId = userId;
|
UserId = userId;
|
||||||
SessionId = sessionId;
|
SessionId = sessionId;
|
||||||
Content = content;
|
Content = content;
|
||||||
Role = role;
|
Role = role;
|
||||||
ModelId = modelId;
|
ModelId = modelId;
|
||||||
|
if (tokenUsage is not null)
|
||||||
|
{
|
||||||
|
this.TokenUsage = tokenUsage.Adapt<TokenUsageValueObject>();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid UserId { get; set; }
|
public Guid UserId { get; set; }
|
||||||
public Guid SessionId { get; set; }
|
public Guid SessionId { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]
|
||||||
public string Content { get; set; }
|
public string Content { get; set; }
|
||||||
|
|
||||||
public string Role { get; set; }
|
public string Role { get; set; }
|
||||||
public decimal DeductCost { get; set; }
|
public decimal DeductCost { get; set; }
|
||||||
public decimal TotalTokens { get; set; }
|
public decimal TotalTokens { get; set; }
|
||||||
public string ModelId { get; set; }
|
public string ModelId { get; set; }
|
||||||
public string Remark { get; set; }
|
public string? Remark { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(IsOwnsOne = true)] public TokenUsageValueObject TokenUsage { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using SqlSugar;
|
using SqlSugar;
|
||||||
using Volo.Abp.Domain.Entities.Auditing;
|
using Volo.Abp.Domain.Entities.Auditing;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.Entities;
|
namespace Yi.Framework.AiHub.Domain.Entities.Chat;
|
||||||
|
|
||||||
[SugarTable("Ai_Session")]
|
[SugarTable("Ai_Session")]
|
||||||
[SugarIndex($"index_{{table}}_{nameof(UserId)}",$"{nameof(UserId)}", OrderByType.Asc)]
|
[SugarIndex($"index_{{table}}_{nameof(UserId)}",$"{nameof(UserId)}", OrderByType.Asc)]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
using Volo.Abp.Domain.Entities.Auditing;
|
using Volo.Abp.Domain.Entities.Auditing;
|
||||||
using Yi.Framework.Core.Data;
|
using Yi.Framework.Core.Data;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.Entities;
|
namespace Yi.Framework.AiHub.Domain.Entities.Model;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ai应用
|
/// ai应用
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
using SqlSugar;
|
using SqlSugar;
|
||||||
using Volo.Abp.Domain.Entities;
|
using Volo.Abp.Domain.Entities;
|
||||||
using Volo.Abp.Domain.Entities.Auditing;
|
|
||||||
using Yi.Framework.Core.Data;
|
using Yi.Framework.Core.Data;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.Entities;
|
namespace Yi.Framework.AiHub.Domain.Entities.Model;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ai模型定义
|
/// ai模型定义
|
||||||
|
|||||||
@@ -9,6 +9,16 @@ namespace Yi.Framework.AiHub.Domain.Entities;
|
|||||||
[SugarTable("Ai_UsageStatistics")]
|
[SugarTable("Ai_UsageStatistics")]
|
||||||
public class UsageStatisticsAggregateRoot : FullAuditedAggregateRoot<Guid>
|
public class UsageStatisticsAggregateRoot : FullAuditedAggregateRoot<Guid>
|
||||||
{
|
{
|
||||||
|
public UsageStatisticsAggregateRoot()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public UsageStatisticsAggregateRoot(Guid userId, string modelId)
|
||||||
|
{
|
||||||
|
UserId = userId;
|
||||||
|
ModelId = modelId;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用户id
|
/// 用户id
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -19,18 +29,34 @@ public class UsageStatisticsAggregateRoot : FullAuditedAggregateRoot<Guid>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string ModelId { get; set; }
|
public string ModelId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 输入使用token使用
|
|
||||||
/// </summary>
|
|
||||||
public decimal InputTokens { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 输出使用token使用
|
|
||||||
/// </summary>
|
|
||||||
public decimal OutputTokens { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 对话次数
|
/// 对话次数
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Number { get; set; }
|
public int UsageTotalNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用输出token总数
|
||||||
|
/// </summary>
|
||||||
|
public int UsageOutputTokenCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用输入总数
|
||||||
|
/// </summary>
|
||||||
|
public int UsageInputTokenCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总token使用数量
|
||||||
|
/// </summary>
|
||||||
|
public int TotalTokenCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增一次聊天统计
|
||||||
|
/// </summary>
|
||||||
|
public void AddOnceChat(int inputTokenCount, int outputTokenCount)
|
||||||
|
{
|
||||||
|
UsageTotalNumber += 1;
|
||||||
|
UsageOutputTokenCount += outputTokenCount;
|
||||||
|
UsageInputTokenCount += inputTokenCount;
|
||||||
|
TotalTokenCount += (outputTokenCount + inputTokenCount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Domain.Entities.ValueObjects;
|
||||||
|
|
||||||
|
public class TokenUsageValueObject
|
||||||
|
{
|
||||||
|
public int OutputTokenCount { get; set; }
|
||||||
|
|
||||||
|
public int InputTokenCount { get; set; }
|
||||||
|
|
||||||
|
public int TotalTokenCount { get; set; }
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ using OpenAI.Chat;
|
|||||||
using Volo.Abp.Domain.Services;
|
using Volo.Abp.Domain.Services;
|
||||||
using Yi.Framework.AiHub.Domain.AiChat;
|
using Yi.Framework.AiHub.Domain.AiChat;
|
||||||
using Yi.Framework.AiHub.Domain.Entities;
|
using Yi.Framework.AiHub.Domain.Entities;
|
||||||
|
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ public class AiGateWayManager : DomainService
|
|||||||
/// <param name="messages"></param>
|
/// <param name="messages"></param>
|
||||||
/// <param name="cancellationToken"></param>
|
/// <param name="cancellationToken"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async IAsyncEnumerable<string> CompleteChatAsync(string modelId, List<ChatMessage> messages,
|
public async IAsyncEnumerable<CompleteChatResponse> CompleteChatAsync(string modelId, List<ChatMessage> messages,
|
||||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var modelDescribe = await GetModelAsync(modelId);
|
var modelDescribe = await GetModelAsync(modelId);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using Volo.Abp.Users;
|
using Volo.Abp.Users;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos;
|
using Yi.Framework.AiHub.Application.Contracts.Dtos;
|
||||||
using Yi.Framework.AiHub.Domain.Entities;
|
using Yi.Framework.AiHub.Domain.Entities;
|
||||||
|
using Yi.Framework.AiHub.Domain.Entities.Chat;
|
||||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.Managers;
|
namespace Yi.Framework.AiHub.Domain.Managers;
|
||||||
@@ -16,15 +17,30 @@ public class AiMessageManager : DomainService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建消息
|
/// 创建系统消息
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sessionId"></param>
|
/// <param name="sessionId"></param>
|
||||||
/// <param name="userId"></param>
|
/// <param name="userId"></param>
|
||||||
/// <param name="input"></param>
|
/// <param name="input"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task CreateMessageAsync(Guid userId, Guid sessionId, MessageInputDto input)
|
public async Task CreateSystemMessageAsync(Guid userId, Guid sessionId, MessageInputDto input)
|
||||||
{
|
{
|
||||||
var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId);
|
input.Role = "system";
|
||||||
|
var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId,input.TokenUsage);
|
||||||
|
await _repository.InsertAsync(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建系统消息
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sessionId"></param>
|
||||||
|
/// <param name="userId"></param>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task CreateUserMessageAsync(Guid userId, Guid sessionId, MessageInputDto input)
|
||||||
|
{
|
||||||
|
input.Role = "user";
|
||||||
|
var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId,input.TokenUsage);
|
||||||
await _repository.InsertAsync(message);
|
await _repository.InsertAsync(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using Medallion.Threading;
|
||||||
|
using Volo.Abp.Domain.Services;
|
||||||
|
using Yi.Framework.AiHub.Domain.Entities;
|
||||||
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
|
|
||||||
|
public class UsageStatisticsManager : DomainService
|
||||||
|
{
|
||||||
|
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _repository;
|
||||||
|
|
||||||
|
private IDistributedLockProvider DistributedLock =>
|
||||||
|
LazyServiceProvider.LazyGetRequiredService<IDistributedLockProvider>();
|
||||||
|
|
||||||
|
public async Task SetUsageAsync(Guid userId, string modelId, int inputTokenCount, int outputTokenCount)
|
||||||
|
{
|
||||||
|
await using (await DistributedLock.AcquireLockAsync($"UsageStatistics:{userId.ToString()}"))
|
||||||
|
{
|
||||||
|
var entity = await _repository._DbQueryable.FirstAsync(x => x.UserId == userId && x.ModelId == modelId);
|
||||||
|
//存在数据,更细
|
||||||
|
if (entity is not null)
|
||||||
|
{
|
||||||
|
entity.AddOnceChat(inputTokenCount, outputTokenCount);
|
||||||
|
await _repository.UpdateAsync(entity);
|
||||||
|
}
|
||||||
|
//不存在插入
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var usage = new UsageStatisticsAggregateRoot(userId, modelId);
|
||||||
|
usage.AddOnceChat(inputTokenCount, outputTokenCount);
|
||||||
|
await _repository.InsertAsync(usage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class LazyServiceProvider
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Azure.AI.OpenAI" Version="2.2.0-beta.4" />
|
<PackageReference Include="Azure.AI.OpenAI" Version="2.2.0-beta.4" />
|
||||||
<PackageReference Include="Volo.Abp.Ddd.Domain" Version="$(AbpVersion)" />
|
<PackageReference Include="Volo.Abp.Ddd.Domain" Version="$(AbpVersion)" />
|
||||||
|
<PackageReference Include="Volo.Abp.DistributedLocking" Version="$(AbpVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ VITE_WEB_ENV = 'development'
|
|||||||
VITE_WEB_BASE_API = '/dev-api'
|
VITE_WEB_BASE_API = '/dev-api'
|
||||||
|
|
||||||
# 本地接口
|
# 本地接口
|
||||||
# VITE_API_URL = http://localhost:19001/api/app
|
VITE_API_URL = http://localhost:19001/api/app
|
||||||
VITE_API_URL=http://ccnetcore.com:19001/api/app
|
#VITE_API_URL=http://ccnetcore.com:19001/api/app
|
||||||
|
|
||||||
# SSO单点登录url
|
# SSO单点登录url
|
||||||
# SSO_SEVER_URL='http://localhost:18001'
|
# SSO_SEVER_URL='http://localhost:18001'
|
||||||
|
|||||||
Reference in New Issue
Block a user