mirror of
https://github.com/corvax-team/ss14-wl.git
synced 2026-02-14 19:29:57 +01:00
Добавлено api для работы с gpt через ProxyAi + некоторые фиксы упаковки клиента
This commit is contained in:
@@ -48,7 +48,7 @@ internal sealed class ChatManager : IChatManager
|
||||
break;
|
||||
|
||||
case ChatSelectChannel.Dead:
|
||||
if (_systems.GetEntitySystemOrNull<GhostSystem>() is {IsGhost: true})
|
||||
if (_systems.GetEntitySystemOrNull<GhostSystem>() is { IsGhost: true })
|
||||
goto case ChatSelectChannel.Local;
|
||||
|
||||
if (_adminMgr.HasFlag(AdminFlags.Admin))
|
||||
|
||||
@@ -2,8 +2,6 @@ using Content.Client.Administration.Managers;
|
||||
using Content.Client.Changelog;
|
||||
using Content.Client.Chat.Managers;
|
||||
using Content.Client.DebugMon;
|
||||
using Content.Client.Corvax.TTS;
|
||||
using Content.Client.Options;
|
||||
using Content.Client.Eui;
|
||||
using Content.Client.Fullscreen;
|
||||
using Content.Client.GhostKick;
|
||||
@@ -37,6 +35,15 @@ using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Replays;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Serialization.Markdown.Sequence;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Utility;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using Robust.Shared.Serialization.Markdown.Value;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
|
||||
namespace Content.Client.Entry
|
||||
{
|
||||
@@ -124,6 +131,12 @@ namespace Content.Client.Entry
|
||||
_prototypeManager.RegisterIgnore("stationGoal"); // Corvax-StationGoal
|
||||
_prototypeManager.RegisterIgnore("ghostRoleRaffleDecider");
|
||||
|
||||
foreach (var item in IgnorePrototypes())
|
||||
{
|
||||
_prototypeManager.RegisterIgnore(item);
|
||||
Logger.Debug(item);
|
||||
}
|
||||
|
||||
_componentFactory.GenerateNetIds();
|
||||
_adminManager.Initialize();
|
||||
_screenshotHook.Initialize();
|
||||
@@ -220,5 +233,46 @@ namespace Content.Client.Entry
|
||||
_debugMonitorManager.FrameUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
//WL-Changes-start
|
||||
private HashSet<string> IgnorePrototypes()
|
||||
{
|
||||
var sequence = new HashSet<string>();
|
||||
|
||||
foreach (var path in _resourceManager.ContentFindFiles("/"))
|
||||
{
|
||||
if (!path.CanonPath.Contains("_SERVER"))
|
||||
continue;
|
||||
|
||||
if (!_resourceManager.TryContentFileRead(path, out var stream))
|
||||
continue;
|
||||
|
||||
using var reader = new StreamReader(stream, EncodingHelpers.UTF8);
|
||||
var documents = DataNodeParser.ParseYamlStream(reader);
|
||||
|
||||
if (documents == null)
|
||||
continue;
|
||||
|
||||
foreach (var document in documents)
|
||||
{
|
||||
var seq = ((SequenceDataNode)document.Root).Sequence;
|
||||
|
||||
foreach (var item in seq)
|
||||
{
|
||||
|
||||
if (item is not MappingDataNode mapping_node)
|
||||
continue;
|
||||
|
||||
if (!mapping_node.TryGet("type", out var node))
|
||||
continue;
|
||||
|
||||
sequence.Add(node.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sequence;
|
||||
}
|
||||
//WL-Changes-end
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Compression;
|
||||
using Robust.Packaging;
|
||||
using Robust.Packaging.AssetProcessing;
|
||||
@@ -117,7 +117,12 @@ public static class ClientPackaging
|
||||
.Union(RobustSharedPackaging.SharedIgnoredResources)
|
||||
.Union(ContentClientIgnoredResources).ToHashSet();
|
||||
|
||||
await RobustSharedPackaging.DoResourceCopy(Path.Combine(contentDir, "Resources"), pass, ignoreSet, cancel: cancel);
|
||||
await /*WL-Changes-start*/WLSharedPackaging/*WL-Changes-end*/.DoResourceCopy(
|
||||
Path.Combine(contentDir, "Resources"),
|
||||
pass,
|
||||
ignoreSet,
|
||||
/*WL-Changes-start*/WLSharedPackaging.ContentClientIgnoredResources,/*WL-Changes-end*/
|
||||
cancel: cancel);
|
||||
}
|
||||
// Corvax-Secrets-End
|
||||
}
|
||||
|
||||
68
Content.Packaging/WLSharedPackaging.cs
Normal file
68
Content.Packaging/WLSharedPackaging.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using Robust.Packaging.AssetProcessing;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Content.Packaging
|
||||
{
|
||||
/// <summary>
|
||||
/// COPIED FROM <see cref="Robust.Packaging.RobustSharedPackaging"/>.
|
||||
/// </summary>
|
||||
public sealed partial class WLSharedPackaging
|
||||
{
|
||||
[GeneratedRegex(@".*_SERVER.*", RegexOptions.Multiline)]
|
||||
private static partial Regex ServerIgnoreRegex();
|
||||
|
||||
public static IReadOnlySet<Regex> ContentClientIgnoredResources { get; } = new HashSet<Regex>
|
||||
{
|
||||
ServerIgnoreRegex()
|
||||
};
|
||||
|
||||
public static Task DoResourceCopy(
|
||||
string diskSource,
|
||||
AssetPass pass,
|
||||
IReadOnlySet<string> ignoreSet,
|
||||
IReadOnlySet<Regex> ignoreRegexes,
|
||||
string targetDir = "",
|
||||
CancellationToken cancel = default)
|
||||
{
|
||||
foreach (var path in Directory.EnumerateFileSystemEntries(diskSource))
|
||||
{
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
|
||||
var filename = Path.GetFileName(path);
|
||||
|
||||
var blacklisted_f = ignoreSet.Contains(filename);
|
||||
|
||||
var blacklisted_r = ignoreRegexes.Any(regex =>
|
||||
{
|
||||
return regex.IsMatch(path);
|
||||
});
|
||||
|
||||
if (blacklisted_r || blacklisted_f)
|
||||
continue;
|
||||
|
||||
var targetPath = Path.Combine(targetDir, filename);
|
||||
if (Directory.Exists(path))
|
||||
CopyDirIntoZip(path, targetPath, pass);
|
||||
else
|
||||
pass.InjectFileFromDisk(targetPath, path);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void CopyDirIntoZip(string directory, string basePath, AssetPass pass)
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
var relPath = Path.GetRelativePath(directory, file);
|
||||
var zipPath = $"{basePath}/{relPath}";
|
||||
|
||||
if (Path.DirectorySeparatorChar != '/')
|
||||
zipPath = zipPath.Replace(Path.DirectorySeparatorChar, '/');
|
||||
|
||||
// Console.WriteLine($"{directory}/{zipPath} -> /{zipPath}");
|
||||
pass.InjectFileFromDisk(zipPath, file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,6 +116,13 @@ public sealed class AlertLevelSystem : EntitySystem
|
||||
return alert.CurrentDelay;
|
||||
}
|
||||
|
||||
//WL-Changes-start
|
||||
public string GetLevelLocString(string level)
|
||||
{
|
||||
return Loc.GetString($"alert-level-{level}");
|
||||
}
|
||||
//WL-Changes-end
|
||||
|
||||
/// <summary>
|
||||
/// Set the alert level based on the station's entity ID.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server._WL.ChatGpt.Managers;
|
||||
using Content.Server.Acz;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Administration.Logs;
|
||||
@@ -115,6 +116,10 @@ namespace Content.Server.Entry
|
||||
_playTimeTracking.Initialize();
|
||||
IoCManager.Resolve<JobWhitelistManager>().Initialize();
|
||||
IoCManager.Resolve<PlayerRateLimitManager>().Initialize();
|
||||
|
||||
//WL-Changes-start
|
||||
IoCManager.Resolve<IChatGptManager>().Initialize();
|
||||
//WL-Changes-end
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,9 @@ using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.NameModifier.Components;
|
||||
using Content.Shared.Power;
|
||||
using Content.Shared._WL.Fax.Events;
|
||||
|
||||
namespace Content.Server.Fax;
|
||||
|
||||
@@ -294,7 +294,8 @@ public sealed class FaxSystem : EntitySystem
|
||||
args.Data.TryGetValue(FaxConstants.FaxPaperLockedData, out bool? locked);
|
||||
|
||||
var printout = new FaxPrintout(content, name, label, prototypeId, stampState, stampedBy, locked ?? false);
|
||||
Receive(uid, printout, args.SenderAddress);
|
||||
|
||||
Receive(uid, printout, args.SenderAddress, component, args.Sender);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -551,7 +552,12 @@ public sealed class FaxSystem : EntitySystem
|
||||
/// Accepts a new message and adds it to the queue to print
|
||||
/// If has parameter "notifyAdmins" also output a special message to admin chat.
|
||||
/// </summary>
|
||||
public void Receive(EntityUid uid, FaxPrintout printout, string? fromAddress = null, FaxMachineComponent? component = null)
|
||||
public void Receive(
|
||||
EntityUid uid,
|
||||
FaxPrintout printout,
|
||||
string? fromAddress = null,
|
||||
FaxMachineComponent? component = null,
|
||||
/*WL-Changes-start*/EntityUid? sender = null/*WL-Changes-end*/)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
@@ -567,6 +573,13 @@ public sealed class FaxSystem : EntitySystem
|
||||
NotifyAdmins(faxName);
|
||||
|
||||
component.PrintingQueue.Enqueue(printout);
|
||||
|
||||
//WL-Changes-start
|
||||
var ev = new FaxRecieveMessageEvent(printout, sender, (uid, component));
|
||||
|
||||
RaiseLocalEvent(uid, ev);
|
||||
RaiseLocalEvent(ev);
|
||||
//WL-Changes-end
|
||||
}
|
||||
|
||||
private void SpawnPaperFromQueue(EntityUid uid, FaxMachineComponent? component = null)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Global usings for Content.Server
|
||||
// Global usings for Content.Server
|
||||
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server._WL.ChatGpt.Managers;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Administration.Managers;
|
||||
@@ -62,6 +63,9 @@ namespace Content.Server.IoC
|
||||
IoCManager.Register<PlayTimeTrackingManager>();
|
||||
IoCManager.Register<UserDbDataManager>();
|
||||
IoCManager.Register<TTSManager>(); // Corvax-TTS
|
||||
//WL-Changes-start
|
||||
IoCManager.Register<IChatGptManager, ChatGptManager>();
|
||||
//WL-Changes-end
|
||||
IoCManager.Register<ServerInfoManager>();
|
||||
IoCManager.Register<PoissonDiskSampler>();
|
||||
IoCManager.Register<DiscordWebhook>();
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
using Content.Shared.Chat;
|
||||
|
||||
namespace Content.Server._WL.Chat
|
||||
{
|
||||
|
||||
}
|
||||
18
Content.Server/_WL/ChatGpt/AIChatPrototype.cs
Normal file
18
Content.Server/_WL/ChatGpt/AIChatPrototype.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt
|
||||
{
|
||||
[Prototype("aiChat")]
|
||||
public sealed partial class AIChatPrototype : IPrototype
|
||||
{
|
||||
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
|
||||
[DataField]
|
||||
public bool UseMemory { get; private set; } = false;
|
||||
|
||||
[DataField(required: true)]
|
||||
public LocId BasePrompt { get; private set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
29
Content.Server/_WL/ChatGpt/Commands/EnableCCAICommand.cs
Normal file
29
Content.Server/_WL/ChatGpt/Commands/EnableCCAICommand.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Content.Server.Administration;
|
||||
using Content.Shared._WL.CCVars;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt.Commands
|
||||
{
|
||||
[AdminCommand(AdminFlags.Round | AdminFlags.Adminchat)]
|
||||
public sealed partial class EnableCCAICommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _configMan = default!;
|
||||
|
||||
public override string Command => "togglecentralcommandai";
|
||||
public override string Description => "Отключает или выключает отправку запросов на ЦК к текстовой нейросети.";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var now = _configMan.GetCVar(WLCVars.IsGptEnabled);
|
||||
|
||||
_configMan.SetCVar(WLCVars.IsGptEnabled, !now);
|
||||
|
||||
var text = now
|
||||
? "выключен"
|
||||
: "включен";
|
||||
shell.WriteLine($"Автоответчик ЦК сейчас: {text}");
|
||||
}
|
||||
}
|
||||
}
|
||||
14
Content.Server/_WL/ChatGpt/Elements/OpenAi/AccountBalance.cs
Normal file
14
Content.Server/_WL/ChatGpt/Elements/OpenAi/AccountBalance.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt.Elements.OpenAi
|
||||
{
|
||||
/// <summary>
|
||||
/// Класс, представляющий баланс аккаунта, на котором куплен апи токен.
|
||||
/// Специфичен...
|
||||
/// </summary>
|
||||
public sealed class AccountBalance
|
||||
{
|
||||
[JsonPropertyName("balance")]
|
||||
public required decimal Balance { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Content.Server._WL.ChatGpt.Elements.OpenAi.Response;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt.Elements.OpenAi.Extensions
|
||||
{
|
||||
public static class GptResponseExt
|
||||
{
|
||||
public static string? GetRawStringResponse(this GptChatResponse response, IRobustRandom random)
|
||||
{
|
||||
var choices = response.Choices;
|
||||
|
||||
if (choices.Length == 0)
|
||||
return null;
|
||||
|
||||
var chosen = random.Pick(choices).Message;
|
||||
|
||||
return chosen.Content ?? chosen.RefusalMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Content.Server.RoundEnd;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt.Elements.OpenAi.Functions
|
||||
{
|
||||
public sealed partial class CallEvacShuttleFunction : ToolFunctionModel
|
||||
{
|
||||
private readonly RoundEndSystem _roundEndSys;
|
||||
|
||||
public override LocId Name => "gpt-command-evac-shuttle-name";
|
||||
public override LocId Description => "gpt-command-evac-shuttle-desc";
|
||||
public override IReadOnlyDictionary<string, Parameter<object>> Parameters => new Dictionary<string, Parameter<object>>()
|
||||
{
|
||||
["call"] = new Parameter<bool>()
|
||||
{
|
||||
Description = "gpt-command-evac-shuttle-arg-call-desc"
|
||||
},
|
||||
|
||||
["time"] = new Parameter<int>()
|
||||
{
|
||||
Description = "gpt-command-evac-shuttle-arg-time-desc",
|
||||
Enum = [1200, 600, 300],
|
||||
Required = false
|
||||
}
|
||||
};
|
||||
public override JsonSchemeType ReturnType => JsonSchemeType.Object;
|
||||
|
||||
public override LocId FallbackMessage => "gpt-command-evac-shuttle-fallback";
|
||||
|
||||
public override string? Invoke(Arguments arguments)
|
||||
{
|
||||
if (!arguments.TryCaste<bool>("call", out var call))
|
||||
return null;
|
||||
|
||||
if (call)
|
||||
{
|
||||
if (!arguments.TryCaste<int>("time", out var time))
|
||||
return null;
|
||||
|
||||
var span = TimeSpan.FromSeconds(time);
|
||||
|
||||
_roundEndSys.RequestRoundEnd(span);
|
||||
|
||||
return Loc.GetString(FallbackMessage, ("time", time), ("call", true));
|
||||
}
|
||||
else
|
||||
{
|
||||
_roundEndSys.CancelRoundEndCountdown();
|
||||
|
||||
return Loc.GetString(FallbackMessage, ("call", false));
|
||||
}
|
||||
}
|
||||
|
||||
public CallEvacShuttleFunction(RoundEndSystem roundEnd)
|
||||
{
|
||||
_roundEndSys = roundEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using Content.Server.Chat.Systems;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt.Elements.OpenAi.Functions
|
||||
{
|
||||
public sealed partial class MadeNotifyFunction : ToolFunctionModel
|
||||
{
|
||||
private readonly ChatSystem _chat;
|
||||
private readonly EntityUid _station;
|
||||
|
||||
public override LocId Name => "gpt-command-made-notify-name";
|
||||
public override LocId Description => "gpt-command-made-notify-desc";
|
||||
public override IReadOnlyDictionary<string, Parameter<object>> Parameters => new Dictionary<string, Parameter<object>>()
|
||||
{
|
||||
["text"] = new Parameter<string>()
|
||||
{
|
||||
Required = true,
|
||||
Description = "gpt-command-made-notify-arg-text-desc"
|
||||
}
|
||||
};
|
||||
public override JsonSchemeType ReturnType => JsonSchemeType.Object;
|
||||
public override LocId FallbackMessage => "gpt-command-made-notify-fallback";
|
||||
|
||||
public override string? Invoke(Arguments arguments)
|
||||
{
|
||||
if (!arguments.TryCaste<string>("text", out var text))
|
||||
return null;
|
||||
|
||||
_chat.DispatchStationAnnouncement(_station, text, Loc.GetString("admin-announce-announcer-default"), colorOverride: Color.Yellow);
|
||||
|
||||
return Loc.GetString(FallbackMessage, ("text", text.Split().FirstOrDefault() ?? ""));
|
||||
}
|
||||
|
||||
public MadeNotifyFunction(ChatSystem chat, EntityUid station)
|
||||
{
|
||||
_chat = chat;
|
||||
_station = station;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using Content.Server.AlertLevel;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt.Elements.OpenAi.Functions
|
||||
{
|
||||
public sealed class SetAlertLevelFunction : ToolFunctionModel
|
||||
{
|
||||
public override LocId Name => "gpt-command-set-alert-level-name";
|
||||
public override LocId Description => "gpt-command-set-alert-level-desc";
|
||||
public override IReadOnlyDictionary<string, Parameter<object>> Parameters => _parameters;
|
||||
public override JsonSchemeType ReturnType => JsonSchemeType.Object;
|
||||
public override LocId FallbackMessage => "gpt-command-set-alert-level-fallback";
|
||||
|
||||
private readonly Dictionary<string, Parameter<object>> _parameters;
|
||||
|
||||
private readonly AlertLevelSystem _alertLevel;
|
||||
private readonly EntityUid _station;
|
||||
private readonly IPrototypeManager _protoMan;
|
||||
|
||||
[ValidatePrototypeId<EntityPrototype>]
|
||||
private static readonly string BaseStationAlertProtoId = "stationAlerts";
|
||||
|
||||
public SetAlertLevelFunction(
|
||||
AlertLevelSystem alertLevelSys,
|
||||
EntityUid station,
|
||||
IPrototypeManager protoMan)
|
||||
{
|
||||
_alertLevel = alertLevelSys;
|
||||
_protoMan = protoMan;
|
||||
_station = station;
|
||||
|
||||
_parameters = Init();
|
||||
}
|
||||
|
||||
public Dictionary<string, Parameter<object>> Init()
|
||||
{
|
||||
var alerts_proto = _protoMan.Index<AlertLevelPrototype>(BaseStationAlertProtoId);
|
||||
|
||||
var levels = alerts_proto.Levels.Keys.ToArray();
|
||||
|
||||
return new()
|
||||
{
|
||||
["level"] = new Parameter<string>()
|
||||
{
|
||||
Enum = levels.ToHashSet() as HashSet<string?>,
|
||||
Description = "gpt-command-set-alert-level-arg-level-desc",
|
||||
Required = true
|
||||
},
|
||||
|
||||
["locked"] = new Parameter<bool>()
|
||||
{
|
||||
Description = "gpt-command-set-alert-level-arg-locked-desc",
|
||||
Required = true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public override string? Invoke(ToolFunctionModel.Arguments arguments)
|
||||
{
|
||||
if (!arguments.TryCaste<string>("level", out var level))
|
||||
return null;
|
||||
|
||||
if (!arguments.TryCaste<bool>("locked", out var locked))
|
||||
return null;
|
||||
|
||||
_alertLevel.SetLevel(_station, level, true, true, true, locked);
|
||||
|
||||
return Loc.GetString(FallbackMessage,
|
||||
("level", level),
|
||||
("locked", locked));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Content.Server._WL.Ert;
|
||||
using Content.Shared._WL.Ert;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt.Elements.OpenAi.Functions
|
||||
{
|
||||
public sealed class ERTSpawnShuttleFunction : ToolFunctionModel
|
||||
{
|
||||
private readonly ErtSystem _ert;
|
||||
|
||||
public override LocId Name => "gpt-command-spawn-ert-shuttle-name";
|
||||
public override LocId Description => "gpt-command-spawn-ert-shuttle-desc";
|
||||
public override JsonSchemeType ReturnType => JsonSchemeType.Object;
|
||||
public override IReadOnlyDictionary<string, Parameter<object>> Parameters => new Dictionary<string, Parameter<object>>()
|
||||
{
|
||||
["type"] = new Parameter<string>()
|
||||
{
|
||||
Required = true,
|
||||
Enum = Enum.GetNames(typeof(ErtType))?
|
||||
.ToHashSet() as HashSet<string?>,
|
||||
Description = "gpt-command-spawn-ert-shuttle-arg-level-desc"
|
||||
}
|
||||
};
|
||||
public override LocId FallbackMessage => "gpt-command-spawn-ert-shuttle-fallback";
|
||||
|
||||
public override string? Invoke(Arguments arguments)
|
||||
{
|
||||
if (!arguments.TryCaste<string>("type", out var parsed))
|
||||
return null;
|
||||
|
||||
if (!Enum.TryParse<ErtType>(parsed, out var result))
|
||||
return null;
|
||||
|
||||
var chosen = Loc.GetString(FallbackMessage, ("type", "other"));
|
||||
|
||||
if (!_ert.TrySpawn(result, out _))
|
||||
return chosen;
|
||||
|
||||
return Loc.GetString(FallbackMessage, ("type", parsed));
|
||||
}
|
||||
|
||||
public ERTSpawnShuttleFunction(ErtSystem ertSys)
|
||||
{
|
||||
_ert = ertSys;
|
||||
}
|
||||
}
|
||||
}
|
||||
110
Content.Server/_WL/ChatGpt/Elements/OpenAi/ModelRole.cs
Normal file
110
Content.Server/_WL/ChatGpt/Elements/OpenAi/ModelRole.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using static Content.Server._WL.ChatGpt.Elements.OpenAi.ModelRole;
|
||||
using static Content.Server._WL.ChatGpt.Elements.OpenAi.ModelRole.Constants;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt.Elements.OpenAi
|
||||
{
|
||||
/// <summary>
|
||||
/// Статический класс, содержащий api для преобразования строковых roles в тип <see cref="ModelRoleType"/>.
|
||||
/// </summary>
|
||||
public static class ModelRole
|
||||
{
|
||||
/// <summary>
|
||||
/// Тип роли при ответе/запросе к модели.
|
||||
/// </summary>
|
||||
public enum ModelRoleType : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Сообщение от системы.
|
||||
/// </summary>
|
||||
System,
|
||||
|
||||
/// <summary>
|
||||
/// Сообщение от пользователя.
|
||||
/// </summary>
|
||||
User,
|
||||
|
||||
/// <summary>
|
||||
/// Н/Д. Я хз что это, но это что-то новое.
|
||||
/// </summary>
|
||||
Assistant,
|
||||
|
||||
/// <summary>
|
||||
/// Роль утилиты.
|
||||
/// </summary>
|
||||
Tool,
|
||||
|
||||
/// <summary>
|
||||
/// Роль функции.
|
||||
/// </summary>
|
||||
[Obsolete("Устарело, используйте методику Tools")]
|
||||
Function,
|
||||
|
||||
/// <summary>
|
||||
/// НЕ ИСПОЛЬЗОВАТЬ ДЛЯ ОТПРАВКИ ЗАПРОСОВ.
|
||||
/// Обычно объект имеет это значение, когда <see cref="FromString(string)"/> не смог определить нужный тип.
|
||||
/// Проверяйте объект на это значение и логгируйте.
|
||||
/// </summary>
|
||||
Invalid
|
||||
}
|
||||
|
||||
public static ModelRoleType FromString(string role)
|
||||
{
|
||||
return role switch
|
||||
{
|
||||
UserRoleString => ModelRoleType.User,
|
||||
SystemRoleString => ModelRoleType.System,
|
||||
ToolRoleString => ModelRoleType.Tool,
|
||||
AssistantRoleString => ModelRoleType.Assistant,
|
||||
#pragma warning disable CS0618
|
||||
FunctionToleString => ModelRoleType.Function,
|
||||
#pragma warning restore
|
||||
_ => ModelRoleType.Invalid
|
||||
};
|
||||
}
|
||||
|
||||
public static bool IsStringValid(string role)
|
||||
{
|
||||
var role_ = FromString(role);
|
||||
|
||||
return role_ != ModelRoleType.Invalid;
|
||||
}
|
||||
|
||||
public static string FromModelRoleType(ModelRoleType role_type)
|
||||
{
|
||||
return role_type switch
|
||||
{
|
||||
ModelRoleType.System => SystemRoleString,
|
||||
ModelRoleType.User => UserRoleString,
|
||||
ModelRoleType.Assistant => AssistantRoleString,
|
||||
ModelRoleType.Tool => ToolRoleString,
|
||||
#pragma warning disable CS0618
|
||||
ModelRoleType.Function => FunctionToleString,
|
||||
#pragma warning restore
|
||||
_ => throw new NotImplementedException($"Невалидный объект перечисления {nameof(ModelRoleType)}")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Константы для текущего класса.
|
||||
/// </summary>
|
||||
public static class Constants
|
||||
{
|
||||
public const string UserRoleString = "user";
|
||||
public const string SystemRoleString = "system";
|
||||
public const string ToolRoleString = "tool";
|
||||
public const string AssistantRoleString = "assistant";
|
||||
public const string FunctionToleString = "function";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Расширения для <see cref="ModelRoleType"/>.
|
||||
/// </summary>
|
||||
public static class ModelRoleTypeExt
|
||||
{
|
||||
public static string ToQueryString(this ModelRoleType type)
|
||||
{
|
||||
return FromModelRoleType(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
54
Content.Server/_WL/ChatGpt/Elements/OpenAi/ModelTool.cs
Normal file
54
Content.Server/_WL/ChatGpt/Elements/OpenAi/ModelTool.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using static Content.Server._WL.ChatGpt.Elements.OpenAi.ModelTool.Constants;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt.Elements.OpenAi
|
||||
{
|
||||
/// <summary>
|
||||
/// Статический класс для преобразований <see cref="ModelToolType"/>.
|
||||
/// </summary>
|
||||
public static class ModelTool
|
||||
{
|
||||
/// <summary>
|
||||
/// Тип "утилиты", используемой для предоставления модели возможности строго делать какие-либо действия в зависимости от сгенерированного контекста.
|
||||
/// </summary>
|
||||
public enum ModelToolType : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Функция.
|
||||
/// </summary>
|
||||
Function,
|
||||
|
||||
/// <summary>
|
||||
/// НЕ ИСПОЛЬЗОВАТЬ ДЛЯ ОТПРАВКИ ЗАПРОСОВ.
|
||||
/// Обычно объект имеет это значение, когда <see cref="FromString(string)"/> не смог определить нужный тип.
|
||||
/// Проверяйте объект на это значение и логгируйте.
|
||||
/// </summary>
|
||||
Invalid
|
||||
}
|
||||
|
||||
public static ModelToolType FromString(string tool)
|
||||
{
|
||||
return tool switch
|
||||
{
|
||||
FunctionToolString => ModelToolType.Function,
|
||||
_ => ModelToolType.Invalid
|
||||
};
|
||||
}
|
||||
|
||||
public static string FromModelToolType(ModelToolType tool_type)
|
||||
{
|
||||
return tool_type switch
|
||||
{
|
||||
ModelToolType.Function => FunctionToolString,
|
||||
_ => throw new NotImplementedException($"Невалидный объект перечисления {nameof(ModelToolType)}")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Константы для текущего класса.
|
||||
/// </summary>
|
||||
public static class Constants
|
||||
{
|
||||
public const string FunctionToolString = "function";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
#pragma warning disable IDE0290
|
||||
|
||||
using Content.Server._WL.ChatGpt.Elements.OpenAi.Response;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt.Elements.OpenAi.Request
|
||||
{
|
||||
/// <summary>
|
||||
/// Класс, используемый для отправки запросов к модели.
|
||||
/// </summary>
|
||||
public abstract class GptChatMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Роль сообщения. Смотреть <see cref="ModelRole.ModelRoleType"/>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("role")]
|
||||
public string Role { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Содержание запроса.
|
||||
/// </summary>
|
||||
[JsonPropertyName("content")]
|
||||
public string Content { get; set; }
|
||||
|
||||
#region ctor
|
||||
protected GptChatMessage(string role, string content)
|
||||
{
|
||||
DebugTools.Assert(ModelRole.IsStringValid(role));
|
||||
|
||||
Role = role;
|
||||
Content = content;
|
||||
}
|
||||
|
||||
protected GptChatMessage(ModelRole.ModelRoleType roleType, string content)
|
||||
: this(ModelRole.FromModelRoleType(roleType), content) { }
|
||||
#endregion
|
||||
|
||||
#region Message types
|
||||
/// <summary>
|
||||
/// Сообщение от пользователя.
|
||||
/// </summary>
|
||||
public sealed class User : GptChatMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Имя, от которого будет отправлено пользовательское сообщение.
|
||||
/// Нужно для различия разных пользователей, если используется "память".
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
public User(string content)
|
||||
: base(ModelRole.ModelRoleType.User, content)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Сообщение от системы.
|
||||
/// </summary>
|
||||
public sealed class System(string content) : GptChatMessage(ModelRole.ModelRoleType.System, content)
|
||||
{
|
||||
/// <summary>
|
||||
/// Имя системы(не виндовс. кхм, бля, я хз).
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Инструмент.
|
||||
/// Используется для оповещения модель о том, что она выбрала какую-то функцию.
|
||||
/// </summary>
|
||||
public sealed class Tool(string content) : GptChatMessage(ModelRole.ModelRoleType.Tool, content)
|
||||
{
|
||||
/// <summary>
|
||||
/// ID выбранной функции.
|
||||
/// Смотреть <see cref="Response.GptChoice.ChoiceMessage.ResponseToolCall.ID"/>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tool_call_id")]
|
||||
public required string ToolId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ассистент, т.е. модель, чат-гпт.
|
||||
/// </summary>
|
||||
/// <param name="content"></param>
|
||||
public sealed class Assistant(string content) : GptChatMessage(ModelRole.ModelRoleType.Assistant, content)
|
||||
{
|
||||
/// <summary>
|
||||
/// Сообщение, которое будет высвечено при блокировке запроса фильтрами модели.
|
||||
/// </summary>
|
||||
[JsonPropertyName("refusal")]
|
||||
public string? Refusal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Имя, от которого будет системное сообщение.
|
||||
/// Н-р: CHAT GPT OPEN AI SIKIBIDI DOP DOP.
|
||||
/// Кхм.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Инструменты, которые вызвала модель.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tool_calls")]
|
||||
public GptChoice.ChoiceMessage.ResponseToolCall[]? Tools { get; set; }
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt.Elements.OpenAi.Request
|
||||
{
|
||||
/// <summary>
|
||||
/// Класс, описывающий инструмент, который может использовать модель при генерации ответа.
|
||||
/// </summary>
|
||||
public sealed class GptChatTool
|
||||
{
|
||||
/// <summary>
|
||||
/// Тип "инструмента".
|
||||
/// Смотреть <see cref="ModelTool.ModelToolType"/>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public required string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Инструмент.
|
||||
/// </summary>
|
||||
[JsonPropertyName("function")]
|
||||
public required object UsingTool { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Внутренний класс, используемый для конкретизации инструмента, который нужно использовать.
|
||||
/// </summary>
|
||||
public abstract class Tool
|
||||
{
|
||||
/// <summary>
|
||||
/// Простая функция.
|
||||
/// </summary>
|
||||
public sealed class Function : Tool
|
||||
{
|
||||
/// <summary>
|
||||
/// Название вызываемой функции.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Описание функции.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Параметры функции.
|
||||
/// </summary>
|
||||
[JsonPropertyName("parameters")]
|
||||
public FunctionArgumentsScheme? Parameters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Следует ли модели генерировать аргументы для функций ЧЁТКО по схеме,
|
||||
/// А то иначе она может немного косячить со схемой.
|
||||
/// </summary>
|
||||
[JsonPropertyName("strict")]
|
||||
public bool? Strict { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Схема, описывающая параметры функции.
|
||||
/// </summary>
|
||||
public sealed class FunctionArgumentsScheme
|
||||
{
|
||||
/// <summary>
|
||||
/// Возвращаемый тип функции.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public required string ReturnType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Обязательные параметры функции.
|
||||
/// </summary>
|
||||
[JsonPropertyName("required")]
|
||||
public string[]? Required { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Сами параметры.
|
||||
/// </summary>
|
||||
[JsonPropertyName("properties")]
|
||||
public Dictionary<string, Property>? Properties { get; set; }
|
||||
|
||||
[JsonPropertyName("additionalProperties")]
|
||||
public bool AdditionalProperties { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Класс, описывающий параметр функции.
|
||||
/// </summary>
|
||||
public sealed class Property
|
||||
{
|
||||
/// <summary>
|
||||
/// Тип параметра.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public required object Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Описание параметра функции.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Константные значения.
|
||||
/// То есть список всех возможных значений аргумента.
|
||||
/// </summary>
|
||||
[JsonPropertyName("enum")]
|
||||
public object?[]? Enum { get; set; } = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt.Elements.OpenAi.Request
|
||||
{
|
||||
/// <summary>
|
||||
/// Класс запроса к текстовой модели OpenAi
|
||||
/// </summary>
|
||||
public sealed class GptChatRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Список сообщений, которые будут отправлены текстовой модели.
|
||||
/// Использовать <see cref="GptChatMessage"/>.
|
||||
/// <see langword="object"/> тут, потому что ебучий JsonSerializer работаит ни таг каг нада.
|
||||
/// </summary>
|
||||
[JsonPropertyName("messages")]
|
||||
public required object[] Messages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Модель, используемая для генерации ответа.
|
||||
/// </summary>
|
||||
[JsonPropertyName("model")]
|
||||
public string Model { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Использовать или нет выходные данные модели после запроса для использования в Model Distillation.
|
||||
/// <see href="https://platform.openai.com/docs/guides/distillation"/>.
|
||||
/// Как я понял..:. улучшает производительность для 'больших' моделей по типу gpt-4o.
|
||||
/// <see langword="false"/> по умолчанию.
|
||||
/// </summary>
|
||||
[JsonPropertyName("store")]
|
||||
public bool? Store { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Вряд ли будет использоваться.
|
||||
/// Но: лучше сувать сюда словарь.
|
||||
/// Эти метаданные будут использоваться для фильтрации всех ответов в специальном пользовательском UI openAi.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public object? Metadata { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Чем выше значение, тем больше вероятность того. что модель повторит одну и ту же строку в ответе.
|
||||
/// Используется только с "памятью".
|
||||
/// По умолчанию - <see langword="0f"/>.
|
||||
/// </summary>
|
||||
[Range(-2.0, 2.0)]
|
||||
[JsonPropertyName("frequency_penalty")]
|
||||
public double? FrequencyPenalty { get; set; } = 0f;
|
||||
|
||||
//Перепишите это поле блйа, нихуя не понял.
|
||||
//Короче, слева номер(?) токена, а справа число принадлежащее [-100, 100], которое влияет на какую-то вероятность(?).
|
||||
/// <summary>
|
||||
/// Accepts a JSON object that maps tokens (specified by their token ID in the tokenizer) to an associated bias value from -100 to 100.
|
||||
/// Mathematically, the bias is added to the logits generated by the model prior to sampling.
|
||||
/// The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection;
|
||||
/// values like -100 or 100 should result in a ban or exclusive selection of the relevant token.
|
||||
/// <see href="https://platform.openai.com/docs/api-reference/chat/create#chat-create-logit_bias"/>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("logit_bias")]
|
||||
public Dictionary<int, int>? LogitBias { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Надо или нет возвращать подробную информацию о каждом токене в ответе.
|
||||
/// <see cref="Response.GptChoice.LogProbs"/>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("logprobs")]
|
||||
public bool? LogProbs { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Целое число от 0 до 20, определяющее количество наиболее вероятных токенов,
|
||||
/// которые будут возвращены в каждой позиции токена, каждый с соответствующей логарифмической вероятностью.
|
||||
/// Для <see cref="LogProbs"/> должно быть установлено значение <see langword="true"/>, если используется этот параметр.
|
||||
/// </summary>
|
||||
[Range(0, 20)]
|
||||
[JsonPropertyName("top_logprobs")]
|
||||
public int? TopLogProbs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Мксимальное количество токенов, которое может быть сгенерировано в отете модели на запрос.
|
||||
/// </summary>
|
||||
[JsonPropertyName("max_completion_tokens")]
|
||||
[Range(0, int.MaxValue)]
|
||||
public int? MaxTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Сколько вариантов ответа предоставит модель при выполнении запроса.
|
||||
/// Смотреть <see cref="Response.GptChatResponse.Choices"/>.
|
||||
/// По умолчанию - <see langword="1"/>.
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue/*Не злоупотребляйте пж*/)]
|
||||
[JsonPropertyName("n")]
|
||||
public int? N { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Чем выше значение, тем больше вероятность того, что модель затронет новую тему.
|
||||
/// </summary>
|
||||
[Range(-2.0, 2.0)]
|
||||
[JsonPropertyName("presence_penalty")]
|
||||
public double? PresencePenalty { get; set; } = 0;
|
||||
|
||||
//[JsonPropertyName("response_format")]
|
||||
//public object? ReponseFormat;
|
||||
|
||||
/// <summary>
|
||||
/// Если указано, то модель при одинаковом сиде будет пытаться ответить на один запрос одинаково.
|
||||
/// Находится в бете.
|
||||
/// </summary>
|
||||
[JsonPropertyName("seed")]
|
||||
public int? Seed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Массив строк, на которые модель, по сути, должна закончить свой ответ.
|
||||
/// "СЛАВА НТ!!!".
|
||||
/// </summary>
|
||||
[JsonPropertyName("stop")]
|
||||
public string[]? Stop { get; set; } = null;
|
||||
|
||||
//[JsonPropertyName("stream")]
|
||||
//public bool? Stream;
|
||||
|
||||
/// <summary>
|
||||
/// Чем выше значение, тем "случайнее" будет результат.
|
||||
/// Если это значение меняется, то менять <see cref="TopP"/> не рекомендуется.
|
||||
/// </summary>
|
||||
[Range(0.0, 2.0)]
|
||||
[JsonPropertyName("temperature")]
|
||||
public double? Temperature { get; set; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// <see href="https://platform.openai.com/docs/api-reference/chat/create#chat-create-top_p"/>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("top_p")]
|
||||
public double? TopP { get; set; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Уникальный идентификатор пользователя, позволяющий избежать абуза запросов.
|
||||
/// Лучше не трогать...
|
||||
/// <see href="https://platform.openai.com/docs/api-reference/chat/create#chat-create-user"/>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("user")]
|
||||
public string? User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Дать или нет возможность вызывать несколько функций за раз.
|
||||
/// Это может повлиять на точность схемы при выборке аргументов для функции, поэтому лучше держать <see langword="false"/>.
|
||||
/// Не должно быть <see langword="null"/>, только если <see cref="Tools"/> не равен <see langword="null"/>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("parallel_tool_calls")]
|
||||
public bool? ParallelToolCalls { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Список функций, которые может вызвать модель в своём ответе.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tools")]
|
||||
public GptChatTool[]? Tools { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Как модель должна выбрать функцию?
|
||||
/// Можно указать, чтобы она не выбирала никаких функций или выбирала конкретную.
|
||||
/// Не должно быть <see langword="null"/>, только если <see cref="Tools"/> не равен <see langword="null"/>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tool_choice")]
|
||||
public string? ModelToolChoice { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Получает сообщения из объектов типа object в данном типе.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public GptChatMessage[] GetMessages()
|
||||
{
|
||||
return Messages.Select(x => (x as GptChatMessage)!).ToArray();
|
||||
}
|
||||
|
||||
#region Utility classes
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public static class ToolChoice
|
||||
{
|
||||
public static string FromType(ToolChoiceType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
ToolChoiceType.Required => "required",
|
||||
ToolChoiceType.None => "none",
|
||||
ToolChoiceType.Auto => "auto",
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
}
|
||||
|
||||
public static string FromObject(ModelTool.ModelToolType type, string name)
|
||||
{
|
||||
if (type is ModelTool.ModelToolType.Function)
|
||||
{
|
||||
var obj = new
|
||||
{
|
||||
type = ModelTool.FromModelToolType(type),
|
||||
function = new
|
||||
{
|
||||
name
|
||||
}
|
||||
};
|
||||
|
||||
return Return(obj);
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
|
||||
static string Return(object obj)
|
||||
{
|
||||
return JsonSerializer.Serialize(obj);
|
||||
}
|
||||
}
|
||||
|
||||
public enum ToolChoiceType : byte
|
||||
{
|
||||
Auto,
|
||||
None,
|
||||
Required
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Operators
|
||||
public static implicit operator GptChatRequest(GptChatMessage[] messages)
|
||||
{
|
||||
return new GptChatRequest()
|
||||
{
|
||||
Messages = messages.ToArray()
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using Content.Server._WL.ChatGpt.Elements.Response;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt.Elements.OpenAi.Response
|
||||
{
|
||||
/// <summary>
|
||||
/// Класс, содержащий поля - в JSON ответе текстовой модели.
|
||||
/// </summary>
|
||||
public sealed class GptChatResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Уникальный идентификатор каждого ответа модели.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public required string ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Список вариантов ответов текстовой модели OpenAi на единственный запрос пользователя.
|
||||
/// </summary>
|
||||
[JsonPropertyName("choices")]
|
||||
public required GptChoice[] Choices { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unix-образное число, показывающее время создания ответа на запрос пользователя.
|
||||
/// </summary>
|
||||
[JsonPropertyName("created")]
|
||||
public required int Created { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Модель, использованная при генерации ответа на запрос.
|
||||
/// </summary>
|
||||
[JsonPropertyName("model")]
|
||||
public required string Model { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Уровень обслуживание(?).
|
||||
/// Будет NULL, если в запросе не было включено соимённое поле.
|
||||
/// </summary>
|
||||
[JsonPropertyName("service_tier")]
|
||||
public string? ServiceTier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Строка, в которой зашифрована конфигурация бэкенд-стороны модели, которая генерировала ответ.
|
||||
/// </summary>
|
||||
[JsonPropertyName("system_fingerprint")]
|
||||
public required string Fingerprint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Объект, использованный для генерации...
|
||||
/// Короче, в данном случае оно всегда 'chat.completion'.
|
||||
/// </summary>
|
||||
[JsonPropertyName("object")]
|
||||
public required string Object { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Информация о расходах токенов, их количестве и т.п.
|
||||
/// </summary>
|
||||
[JsonPropertyName("usage")]
|
||||
public required GptTokenUsage Usage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Возвращает чисто ответ модели.
|
||||
/// А зачем нужна другая информация, действительно.
|
||||
/// </summary>
|
||||
/// <returns>NULL, если модель ничего не сгенерировала.</returns>
|
||||
public string? GetRawStringResponse()
|
||||
{
|
||||
if (Choices.Length == 0)
|
||||
return null;
|
||||
|
||||
var chosen = Choices[0].Message;
|
||||
|
||||
return chosen.Content ?? chosen.RefusalMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using Content.Server._WL.ChatGpt.Elements.OpenAi.Request;
|
||||
using static Content.Server._WL.ChatGpt.Elements.OpenAi.Response.GptChoice.Constants;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt.Elements.OpenAi.Response
|
||||
{
|
||||
/// <summary>
|
||||
/// Класс, характеризующий один из вариантов ответа текстовой модели OpenAi.
|
||||
/// </summary>
|
||||
public sealed class GptChoice
|
||||
{
|
||||
/// <summary>
|
||||
/// Каждое завершение ответа на запрос пользователя сопровождается кодом.
|
||||
/// В этом коде описана причина завершения.
|
||||
/// Смотреть <see cref="FinishType"/> и <see cref="FromFinishString()"/>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("finish_reason")]
|
||||
public string? FinishReason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Идентификатор выбора в массиве выборов.
|
||||
/// </summary>
|
||||
[JsonPropertyName("index")]
|
||||
public required int Index { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Сгенерированное моделью сообщение в ответ на запрос пользователя.
|
||||
/// </summary>
|
||||
[JsonPropertyName("message")]
|
||||
public required ChoiceMessage Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Более подробная информация о текущем выборе.
|
||||
/// </summary>
|
||||
[JsonPropertyName("logprobs")]
|
||||
public LogProbabilities? LogProbs { get; set; }
|
||||
|
||||
#region Methods api
|
||||
/// <summary>
|
||||
/// Для объекта класса.
|
||||
/// Также есть статическая версия <see cref="FromFinishString(in string)"/>.
|
||||
/// </summary>
|
||||
public FinishType FromFinishString()
|
||||
{
|
||||
return FromFinishString(FinishReason);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Статическая версия метода <see cref="FromFinishString()"/>.
|
||||
/// </summary>
|
||||
/// <param name="finish_string">Строка-код завершения ответа модели. <see cref="FinishType"/>.</param>
|
||||
/// <returns>Если входной параметр не подошёл ни под один тип завершения, то возвращается <see cref="FinishType.Null"/>.</returns>
|
||||
public static FinishType FromFinishString(in string? finish_string)
|
||||
{
|
||||
return finish_string switch
|
||||
{
|
||||
StopFinishString => FinishType.Stop,
|
||||
LengthFinishString => FinishType.Length,
|
||||
FunctionFinishString => FinishType.FunctionCall,
|
||||
FilterFinishString => FinishType.ContentFilter,
|
||||
ToolFinishString => FinishType.ToolCall,
|
||||
_ => FinishType.Null
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Utility classes and other
|
||||
|
||||
#region Choice message
|
||||
/// <summary>
|
||||
/// Содержит информацию об ответе текстовой модели на запрос.
|
||||
/// </summary>
|
||||
public sealed class ChoiceMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Собственно... Ответ модели.
|
||||
/// </summary>
|
||||
[JsonPropertyName("content")]
|
||||
public string? Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Если <see cref="FinishReason"/> имеет тип <see cref="FinishType.ContentFilter"/>,
|
||||
/// То модель ответит тем, что не может ответить на заданный запрос.
|
||||
/// В этой переменной и содержится этот ответ.
|
||||
/// </summary>
|
||||
[JsonPropertyName("refusal")]
|
||||
public string? RefusalMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Роль сообщения. Смотреть <see cref="ModelRole"/>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("role")]
|
||||
public required string Role { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Список вызванных функций.
|
||||
/// Может быть NULL, если на выбор модели не было предоставлено каких-либо функций.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tool_calls")]
|
||||
public ResponseToolCall[]? Tools { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Превращает ответ модели в объект <see cref="GptChatMessage"/>.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public GptChatMessage ToChatMessage(string? name = null)
|
||||
{
|
||||
var role = ModelRole.FromString(Role);
|
||||
var content = Content ?? RefusalMessage ?? string.Empty;
|
||||
|
||||
return role switch
|
||||
{
|
||||
ModelRole.ModelRoleType.User => new GptChatMessage.User(content)
|
||||
{
|
||||
Name = name
|
||||
},
|
||||
ModelRole.ModelRoleType.System => new GptChatMessage.System(content)
|
||||
{
|
||||
Name = name
|
||||
},
|
||||
ModelRole.ModelRoleType.Assistant => new GptChatMessage.Assistant(content)
|
||||
{
|
||||
Refusal = RefusalMessage,
|
||||
Name = name,
|
||||
Tools = Tools
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Класс, содержащий информацию о вызванных моделью функциях.
|
||||
/// </summary>
|
||||
public sealed class ResponseToolCall
|
||||
{
|
||||
/// <summary>
|
||||
/// ID ответа.
|
||||
/// Абсолютно случайное и ни к чему не привязано.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public required string ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Тип "утилиты".
|
||||
/// На данный момент есть только функция.
|
||||
/// Смотреть в <see cref="ModelTool.ModelToolType"/>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public required string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Сама функция.
|
||||
/// </summary>
|
||||
[JsonPropertyName("function")]
|
||||
public required FunctionResponseCall Function { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Информация о вызове функции моделью.
|
||||
/// </summary>
|
||||
public sealed class FunctionResponseCall
|
||||
{
|
||||
/// <summary>
|
||||
/// Имя функции.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Переданные в функцию параметры.
|
||||
/// </summary>
|
||||
[JsonPropertyName("arguments")]
|
||||
public string? Arguments { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Вытаскивает словарь названий и значений выбранных аргументов.
|
||||
/// Не вызывайте туеву тучу раз!!
|
||||
/// Вызвали, кешировали, профит.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public JsonObject? ParseArguments()
|
||||
{
|
||||
if (Arguments == null)
|
||||
return null;
|
||||
|
||||
var node = JsonNode.Parse(Arguments);
|
||||
|
||||
return node?.AsObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Log probs
|
||||
/// <summary>
|
||||
/// Класс, содержащий подробную информацию о выборе модели при ответе на запрос.
|
||||
/// По большей части тут содержится информация о каждом токене в запросе к/ответе модели.
|
||||
/// </summary>
|
||||
public sealed class LogProbabilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Информация о каждом токене ответа/запроса.
|
||||
/// </summary>
|
||||
[JsonPropertyName("content")]
|
||||
public Content[]? LogContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Информация о каждом из отклонённых(?, я честно хз за что отвечает это поле. Перепишите кто-нибудь.) токенов ответа/запроса.
|
||||
/// </summary>
|
||||
[JsonPropertyName("refusal")]
|
||||
public Content[]? LogRefusalContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// В этом классе содержится информация об токене: вероятность выбора, байтовое представление.
|
||||
/// </summary>
|
||||
[Virtual]
|
||||
public class Content
|
||||
{
|
||||
/// <summary>
|
||||
/// Сам токен.
|
||||
/// </summary>
|
||||
[JsonPropertyName("token")]
|
||||
public required string Token { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Вероятность выбора токена.
|
||||
/// </summary>
|
||||
[JsonPropertyName("logprob")]
|
||||
public required float LogProb { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Байтовое представление токена.
|
||||
/// Кодировка - UTF8.
|
||||
/// Может иметь NULL, если токен не имеет байтового представления.
|
||||
/// </summary>
|
||||
[JsonPropertyName("bytes")]
|
||||
public byte[]? Bytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="TopContent"/>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("top_logprobs")]
|
||||
public required TopContent[] TopLogProb { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List of the most likely tokens and their log probability, at this token position.
|
||||
/// In rare cases, there may be fewer than the number of requested top_logprobs returned.
|
||||
/// </summary>
|
||||
public sealed class TopContent : Content;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Finish type
|
||||
/// <summary>
|
||||
/// Перечисление, которое содержит строковые коды окончания ответа модели.
|
||||
/// </summary>
|
||||
public enum FinishType : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Модель полностью ответила на запрос, либо сообщение остановлено одной из последовательностей остановки.
|
||||
/// </summary>
|
||||
Stop,
|
||||
|
||||
/// <summary>
|
||||
/// Неполный вывод модели из-за параметра <see cref="GptChatRequest.MaxTokens"/> или из-за лимита токенов.
|
||||
/// </summary>
|
||||
Length,
|
||||
|
||||
/// <summary>
|
||||
/// Модель вызвала функцию.
|
||||
/// </summary>
|
||||
[Obsolete($"Сейчас чаще используется методика ToolCalls")]
|
||||
FunctionCall,
|
||||
|
||||
/// <summary>
|
||||
/// Модель не смогла ответить на запрос из-за фильтров содержимого(NSFW).
|
||||
/// </summary>
|
||||
ContentFilter,
|
||||
|
||||
/// <summary>
|
||||
/// Модель вызвала функцию.
|
||||
/// </summary>
|
||||
ToolCall,
|
||||
|
||||
/// <summary>
|
||||
/// Ответ всё еще выполняется, или он неполный.
|
||||
/// Либо ответ не подошёл под другие коды, в последнем случае просмотрите логи.
|
||||
/// </summary>
|
||||
Null
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constants
|
||||
/// <summary>
|
||||
/// Содержит константы для класса <see cref="GptChoice"/>.
|
||||
/// </summary>
|
||||
public static class Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// Строковое представление типа окончания диалога с кодом "STOP".
|
||||
/// </summary>
|
||||
public const string StopFinishString = "stop";
|
||||
|
||||
/// <summary>
|
||||
/// Строковое представление типа окончания диалога с кодом "LENGTH".
|
||||
/// </summary>
|
||||
public const string LengthFinishString = "length";
|
||||
|
||||
/// <summary>
|
||||
/// Строковое представление типа окончания диалога с кодом "FUNCTION_CALL".
|
||||
/// </summary>
|
||||
public const string FunctionFinishString = "function_call";
|
||||
|
||||
/// <summary>
|
||||
/// Строковое представление типа окончания диалога с кодом "CONTENT_FILTER".
|
||||
/// </summary>
|
||||
public const string FilterFinishString = "content_filter";
|
||||
|
||||
/// <summary>
|
||||
/// Строковое представление типа окончания диалога с кодом "TOOL_CALLS".
|
||||
/// </summary>
|
||||
public const string ToolFinishString = "tool_calls";
|
||||
}
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt.Elements.Response
|
||||
{
|
||||
/// <summary>
|
||||
/// Класс характеризующий информацию о входящих и исходящих токенах.
|
||||
/// </summary>
|
||||
public sealed class GptTokenUsage
|
||||
{
|
||||
/// <summary>
|
||||
/// Количество токенов в промте.
|
||||
/// </summary>
|
||||
[JsonPropertyName("prompt_tokens")]
|
||||
public required int InputTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Количество токенов в ответе модели.
|
||||
/// </summary>
|
||||
[JsonPropertyName("completion_tokens")]
|
||||
public required int OutputTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Общее количество токенов.
|
||||
/// Сумма <see cref="InputTokens"/> и <see cref="OutputTokens"/>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("total_tokens")]
|
||||
public required int TotalTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Breakdown of tokens used in a completion.
|
||||
/// </summary>
|
||||
[JsonPropertyName("completion_tokens_details")]
|
||||
public CompletionTokensDetail? CompletionDetail { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[JsonPropertyName("prompt_tokens_details")]
|
||||
public PromptTokensDetail? PromptDetail { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Класс, использующийся для подробного описания количества токенов в ответе модели.
|
||||
/// </summary>
|
||||
public sealed class CompletionTokensDetail
|
||||
{
|
||||
/// <summary>
|
||||
/// Audio input tokens generated by the model.
|
||||
/// </summary>
|
||||
[JsonPropertyName("audio_tokens")]
|
||||
public int? AudioTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tokens generated by the model for reasoning.
|
||||
/// </summary>
|
||||
[JsonPropertyName("reasoning_tokens")]
|
||||
public int? ReasoningTokens { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Класс, использующийся для подробного описания количества токенов в запросе к модели.
|
||||
/// </summary>
|
||||
public sealed class PromptTokensDetail
|
||||
{
|
||||
/// <summary>
|
||||
/// Audio input tokens present in the prompt.
|
||||
/// </summary>
|
||||
[JsonPropertyName("audio_tokens")]
|
||||
public int? AudioTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cached tokens present in the prompt.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cached_tokens")]
|
||||
public int? CachedTokens { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
223
Content.Server/_WL/ChatGpt/Elements/OpenAi/ToolFunctionModel.cs
Normal file
223
Content.Server/_WL/ChatGpt/Elements/OpenAi/ToolFunctionModel.cs
Normal file
@@ -0,0 +1,223 @@
|
||||
using Content.Server._WL.ChatGpt.Elements.OpenAi.Request;
|
||||
using Content.Server._WL.ChatGpt.Elements.OpenAi.Response;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt.Elements.OpenAi
|
||||
{
|
||||
public abstract class ToolFunctionModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Имя метода.
|
||||
/// </summary>
|
||||
public abstract LocId Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Описание метода.
|
||||
/// </summary>
|
||||
public abstract LocId Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Сообщени
|
||||
/// </summary>
|
||||
public abstract LocId FallbackMessage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Аргументы метода.
|
||||
/// </summary>
|
||||
public abstract IReadOnlyDictionary<string, Parameter<object>> Parameters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Метод вызова... метода, кхм.
|
||||
/// </summary>
|
||||
/// <param name="arguments">Аргументы, переданные в эту функцию.</param>
|
||||
/// <returns>Строку-статус, который будет передан текстовой модели для понимания того, что она сделала.</returns>
|
||||
public abstract string? Invoke(Arguments arguments);
|
||||
|
||||
/// <summary>
|
||||
/// Тип объекта, возвращаемый функцией.
|
||||
/// </summary>
|
||||
public abstract JsonSchemeType ReturnType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Превращает объект <see cref="ToolFunctionModel"/> в <see cref="GptChatTool"/>.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public GptChatTool GetToolFunction()
|
||||
{
|
||||
var required = Parameters
|
||||
//.Where(x => x.Value.Required)
|
||||
.Select(x => x.Key)
|
||||
.ToArray();
|
||||
|
||||
var properties = Parameters
|
||||
.ToDictionary(k => k.Key, v =>
|
||||
{
|
||||
var desc = v.Value.Description;
|
||||
|
||||
var string_type = v.Value.Type.ToString();
|
||||
|
||||
var type = (object)(v.Value.Required
|
||||
? string_type
|
||||
: new List<string>() { string_type, "null" });
|
||||
|
||||
var property = new GptChatTool.Tool.Function.FunctionArgumentsScheme.Property()
|
||||
{
|
||||
Description = desc == null ? null : Loc.GetString(desc),
|
||||
Enum = v.Value.Enum?.ToArray(),
|
||||
Type = type
|
||||
};
|
||||
|
||||
return property;
|
||||
});
|
||||
|
||||
var function = new GptChatTool.Tool.Function()
|
||||
{
|
||||
Name = Loc.GetString(Name),
|
||||
Description = Loc.GetString(Description),
|
||||
Strict = true,
|
||||
Parameters = new()
|
||||
{
|
||||
ReturnType = ReturnType.ToFormatString(),
|
||||
Required = required,
|
||||
Properties = properties
|
||||
}
|
||||
};
|
||||
|
||||
var tool = new GptChatTool()
|
||||
{
|
||||
Type = ModelTool.Constants.FunctionToolString,
|
||||
UsingTool = function
|
||||
};
|
||||
|
||||
return tool;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Статический метод, позволяющий сгруппировать класс инструмента функции и ответ с инструментом от модели.
|
||||
/// </summary>
|
||||
/// <param name="response">Список ответов модели.</param>
|
||||
/// <param name="models">Список переданных функций.</param>
|
||||
/// <returns></returns>
|
||||
public static List<(GptChoice.ChoiceMessage.ResponseToolCall Response, ToolFunctionModel Model)> GiveChosenModels(
|
||||
IEnumerable<GptChoice.ChoiceMessage.ResponseToolCall> response,
|
||||
IEnumerable<ToolFunctionModel> models)
|
||||
{
|
||||
var list = new List<(GptChoice.ChoiceMessage.ResponseToolCall Response, ToolFunctionModel Model)>();
|
||||
|
||||
foreach (var tool_call in response)
|
||||
{
|
||||
foreach (var model in models)
|
||||
{
|
||||
if (tool_call.Function.Name == Loc.GetString(model.Name))
|
||||
{
|
||||
list.Add((tool_call, model));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Класс, описывающий параметр метода.
|
||||
/// </summary>
|
||||
public sealed class Parameter<T> where T : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// Используется для сохранения типа параметра после приведения <see cref="T"/> к <see langword="object"/>.
|
||||
/// </summary>
|
||||
private Type _type;
|
||||
|
||||
/// <summary>
|
||||
/// Строковое представление типа в формате JSON scheme.
|
||||
/// </summary>
|
||||
public string Type => _type.ToJsonSchemeString();
|
||||
|
||||
/// <summary>
|
||||
/// Описание аргумента.
|
||||
/// </summary>
|
||||
public LocId? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Набор константных значений аргумента.
|
||||
/// </summary>
|
||||
public HashSet<T?>? Enum { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Обязателен ли этот аргумент?
|
||||
/// </summary>
|
||||
public bool Required { get; set; } = true;
|
||||
|
||||
public static implicit operator Parameter<object>(Parameter<T> parameter)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Enum = parameter.Enum?
|
||||
.Select(x => (object?)x)
|
||||
.ToHashSet(),
|
||||
Description = parameter.Description,
|
||||
Required = parameter.Required,
|
||||
_type = parameter._type
|
||||
};
|
||||
}
|
||||
|
||||
public Parameter()
|
||||
{
|
||||
_type = typeof(T);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Класс, описывающий аргументы метода.
|
||||
/// </summary>
|
||||
public sealed class Arguments
|
||||
{
|
||||
private readonly JsonNode _node;
|
||||
|
||||
public T? Caste<T>(string element)
|
||||
{
|
||||
TryCaste<T>(element, out var parsed);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Достаёт из словаря аргумент и приводит к выбранному типу.
|
||||
/// При неудаче возвращает <see langword="false"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="element"></param>
|
||||
/// <param name="parsed"></param>
|
||||
/// <returns></returns>
|
||||
public bool TryCaste<T>(string element, [NotNullWhen(true)] out T? parsed)
|
||||
{
|
||||
parsed = default;
|
||||
|
||||
var node = _node[element];
|
||||
if (node == null)
|
||||
return false;
|
||||
|
||||
var parsed_t = node.GetValue<T>();
|
||||
if (parsed_t == null)
|
||||
return false;
|
||||
|
||||
parsed = parsed_t;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Arguments FromNode(JsonNode node)
|
||||
{
|
||||
return new Arguments(node);
|
||||
}
|
||||
|
||||
private Arguments(JsonNode node)
|
||||
{
|
||||
_node = node;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
Content.Server/_WL/ChatGpt/JsonSchemeType.cs
Normal file
49
Content.Server/_WL/ChatGpt/JsonSchemeType.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
namespace Content.Server._WL.ChatGpt
|
||||
{
|
||||
public enum JsonSchemeType : byte
|
||||
{
|
||||
String,
|
||||
Integer,
|
||||
Number,
|
||||
Boolean,
|
||||
Array,
|
||||
Object,
|
||||
Null
|
||||
}
|
||||
|
||||
public static class JsonSchemeTypeExt
|
||||
{
|
||||
public static string ToFormatString(this JsonSchemeType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
JsonSchemeType.Integer => "integer",
|
||||
JsonSchemeType.Number => "number",
|
||||
JsonSchemeType.Boolean => "boolean",
|
||||
JsonSchemeType.Array => "array",
|
||||
JsonSchemeType.String => "string",
|
||||
JsonSchemeType.Object => "object",
|
||||
JsonSchemeType.Null => "null",
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
}
|
||||
|
||||
public static JsonSchemeType ToJsonSchemeType(this Type type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
_ when type == typeof(int) => JsonSchemeType.Integer,
|
||||
_ when type == typeof(bool) => JsonSchemeType.Boolean,
|
||||
_ when type == typeof(float) || type == typeof(double) || type == typeof(decimal) => JsonSchemeType.Number,
|
||||
_ when type == typeof(string) => JsonSchemeType.String,
|
||||
_ when type.IsArray => JsonSchemeType.Array,
|
||||
_ => JsonSchemeType.Object
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToJsonSchemeString(this Type type)
|
||||
{
|
||||
return type.ToJsonSchemeType().ToFormatString();
|
||||
}
|
||||
}
|
||||
}
|
||||
266
Content.Server/_WL/ChatGpt/Managers/ChatGptManager.cs
Normal file
266
Content.Server/_WL/ChatGpt/Managers/ChatGptManager.cs
Normal file
@@ -0,0 +1,266 @@
|
||||
using Content.Server._WL.ChatGpt.Elements.OpenAi;
|
||||
using Content.Server._WL.ChatGpt.Elements.OpenAi.Request;
|
||||
using Content.Server._WL.ChatGpt.Elements.OpenAi.Response;
|
||||
using Content.Shared._WL.CCVars;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt.Managers
|
||||
{
|
||||
public sealed partial class ChatGptManager : IChatGptManager, IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly ILogManager _logMan = default!;
|
||||
[Dependency] private readonly IConfigurationManager _confMan = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
public const string SawmillName = "chat.gpt.mngr";
|
||||
|
||||
private const string AIDisabledDeclineMessage = "Использование API нейросетей на данный момент выключено.";
|
||||
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
IgnoreReadOnlyFields = true,
|
||||
IgnoreReadOnlyProperties = true,
|
||||
WriteIndented = true,
|
||||
UnmappedMemberHandling = System.Text.Json.Serialization.JsonUnmappedMemberHandling.Skip,
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
private static readonly TimeSpan QueryTimeout = TimeSpan.FromSeconds(15);
|
||||
|
||||
private HttpClient _httpClient = default!;
|
||||
|
||||
private string _apiKey = default!;
|
||||
private string _endpoint = default!;
|
||||
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
private bool _enabled = default!;
|
||||
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
private string _chatModel = default!;
|
||||
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
private int _maxResponseTokens = default!;
|
||||
|
||||
private Uri _balanceMap = default!;
|
||||
|
||||
#region Init stuff
|
||||
public void Initialize()
|
||||
{
|
||||
_httpClient = new HttpClient()
|
||||
{
|
||||
Timeout = QueryTimeout
|
||||
};
|
||||
|
||||
SetupCVars();
|
||||
|
||||
var endpoint = new Uri(_endpoint, UriKind.Absolute);
|
||||
|
||||
_httpClient.BaseAddress = endpoint;
|
||||
}
|
||||
|
||||
private void SetupCVars()
|
||||
{
|
||||
// Api key
|
||||
_apiKey = _confMan.GetCVar(WLCVars.GptApiKey);
|
||||
_confMan.OnValueChanged(WLCVars.GptApiKey, (value) => _apiKey = value, true);
|
||||
|
||||
// Endpoint
|
||||
_endpoint = _confMan.GetCVar(WLCVars.GptQueriesEndpoint);
|
||||
_confMan.OnValueChanged(WLCVars.GptQueriesEndpoint, (value) =>
|
||||
{
|
||||
_endpoint = value;
|
||||
}, true);
|
||||
|
||||
// Chat model
|
||||
_chatModel = _confMan.GetCVar(WLCVars.GptChatModel);
|
||||
_confMan.OnValueChanged(WLCVars.GptChatModel, (new_model) =>
|
||||
{
|
||||
_chatModel = new_model;
|
||||
}, true);
|
||||
|
||||
//Max tokens
|
||||
_enabled = _confMan.GetCVar(WLCVars.IsGptEnabled);
|
||||
_confMan.OnValueChanged(WLCVars.IsGptEnabled, (enable) => _enabled = enable, true);
|
||||
|
||||
//Max tokens
|
||||
_maxResponseTokens = _confMan.GetCVar(WLCVars.GptMaxTokens);
|
||||
_confMan.OnValueChanged(WLCVars.GptMaxTokens, (max) => _maxResponseTokens = max, true);
|
||||
|
||||
//Balance
|
||||
_balanceMap = new(_confMan.GetCVar(WLCVars.GptBalanceMap));
|
||||
_confMan.OnValueChanged(WLCVars.GptBalanceMap, (map) => _balanceMap = new(map), true);
|
||||
}
|
||||
|
||||
public void PostInject()
|
||||
{
|
||||
_sawmill = _logMan.GetSawmill(SawmillName);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public api
|
||||
/// <summary>
|
||||
/// Показывает можно ли сейчас отправлять запросы к ИИ.
|
||||
/// </summary>
|
||||
public bool IsEnabled()
|
||||
{
|
||||
return _enabled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IsEnabled()" />.
|
||||
/// </summary>
|
||||
/// <param name="reason">Сообщение, когда ИИ выключен. <see cref="AIDisabledDeclineMessage" />.</param>
|
||||
public bool IsEnabled([NotNullWhen(false)] out string? reason)
|
||||
{
|
||||
reason = null;
|
||||
|
||||
if (!_enabled)
|
||||
reason = AIDisabledDeclineMessage;
|
||||
|
||||
return _enabled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает оставшееся количество рублей на счёте(((
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<decimal> GetBalanceAsync(CancellationToken cancel = default)
|
||||
{
|
||||
using var http = new HttpRequestMessage(HttpMethod.Get, _balanceMap);
|
||||
|
||||
AddDefaultsHeaders(http);
|
||||
|
||||
try
|
||||
{
|
||||
var resp = await _httpClient.SendAsync(http, cancel).ConfigureAwait(false);
|
||||
|
||||
resp.EnsureSuccessStatusCode();
|
||||
|
||||
var balance_obj = await resp.Content.ReadFromJsonAsync<AccountBalance>(cancel).ConfigureAwait(false) ??
|
||||
throw new JsonException($"Неудачная сериализация в объект {nameof(AccountBalance)}!");
|
||||
|
||||
return balance_obj.Balance;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error("Ошибка при получении баланса аккаунта ProxyAi!");
|
||||
_sawmill.Error(ex.ToStringBetter());
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>.
|
||||
/// Отправляет запрос к выбранной модели.
|
||||
/// </summary>
|
||||
/// <returns>МОЖЕТ вернуть null, если использование нейросетей в КВарах выключено.</returns>
|
||||
public async Task<GptChatResponse> SendChatQueryAsync(
|
||||
GptChatRequest gpt_request,
|
||||
IEnumerable<ToolFunctionModel>? methods = null,
|
||||
CancellationToken cancel = default)
|
||||
{
|
||||
using var http_request = new HttpRequestMessage(HttpMethod.Post, _httpClient.BaseAddress);
|
||||
|
||||
try
|
||||
{
|
||||
gpt_request.Model = _chatModel;
|
||||
gpt_request.MaxTokens = _maxResponseTokens;
|
||||
|
||||
if (methods != null)
|
||||
{
|
||||
var list = methods.Select(m => m.GetToolFunction());
|
||||
gpt_request.Tools = list.ToArray();
|
||||
}
|
||||
|
||||
AddDefaultsHeaders(http_request);
|
||||
|
||||
var rs = RStopwatch.StartNew();
|
||||
|
||||
var body = JsonSerializer.Serialize(gpt_request, SerializerOptions);
|
||||
|
||||
http_request.Content = new StringContent(body, null, "application/json");
|
||||
|
||||
using var resp = await _httpClient.SendAsync(http_request, cancel);
|
||||
|
||||
var resp_string = await resp.Content.ReadAsStringAsync(cancel);
|
||||
|
||||
try
|
||||
{
|
||||
var gpt_resp = JsonSerializer.Deserialize<GptChatResponse>(resp_string, SerializerOptions);
|
||||
if (gpt_resp == null)
|
||||
{
|
||||
_sawmill.Fatal("При десериализации ответа от модели произошла ошибка! Десериализованное значение равнялось NULL!");
|
||||
throw new HttpRequestException(resp_string, null, resp.StatusCode);
|
||||
}
|
||||
|
||||
LogInf(gpt_resp);
|
||||
|
||||
return gpt_resp;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Fatal($"Ошибка при отправке запроса! Полученный ответ: {resp_string}");
|
||||
_sawmill.Fatal(ex.ToStringBetter());
|
||||
throw;
|
||||
}
|
||||
|
||||
void LogInf(GptChatResponse resp)
|
||||
{
|
||||
var elapsed = rs.Elapsed.Milliseconds;
|
||||
_sawmill.Info($"Запрос {resp.ID} был обработан моделью {resp.Model} за {elapsed}мс. Количество входных токенов: {resp.Usage.InputTokens}. " +
|
||||
$"Количество выходных токенов: {resp.Usage.OutputTokens}. " +
|
||||
$"Общее количество токенов: {resp.Usage.TotalTokens}.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Fatal(ex.ToStringBetter());
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Более простая перегрузка метода <see cref="SendChatQueryAsync(GptChatRequest, CancellationToken)"/>.
|
||||
/// </summary>
|
||||
/// <param name="prompt">Текстовый промт.</param>
|
||||
/// <returns>Возвращает только ответ ИИ. Никакой памяти. Никаких списков. Всё просто.</returns>
|
||||
public async Task<string?> SendChatQuery(string prompt)
|
||||
{
|
||||
var msg = new GptChatMessage.User(prompt);
|
||||
|
||||
var req = new GptChatRequest()
|
||||
{
|
||||
Messages = [msg]
|
||||
};
|
||||
|
||||
var resp = await SendChatQueryAsync(req);
|
||||
|
||||
return resp.GetRawStringResponse();
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Utility
|
||||
/// <summary>
|
||||
/// Добавляет заголовки по-умолчанию для каждого запроса.
|
||||
/// </summary>
|
||||
private void AddDefaultsHeaders(HttpRequestMessage msg)
|
||||
{
|
||||
msg.Headers.Authorization = new("Bearer", _apiKey);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
26
Content.Server/_WL/ChatGpt/Managers/IChatGptManager.cs
Normal file
26
Content.Server/_WL/ChatGpt/Managers/IChatGptManager.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Content.Server._WL.ChatGpt.Elements.OpenAi.Request;
|
||||
using Content.Server._WL.ChatGpt.Elements.OpenAi.Response;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using Content.Server._WL.ChatGpt.Elements.OpenAi;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt.Managers
|
||||
{
|
||||
public interface IChatGptManager
|
||||
{
|
||||
void Initialize();
|
||||
void PostInject();
|
||||
|
||||
bool IsEnabled();
|
||||
bool IsEnabled([NotNullWhen(false)] out string? reason);
|
||||
Task<GptChatResponse> SendChatQueryAsync(
|
||||
GptChatRequest gpt_request,
|
||||
IEnumerable<ToolFunctionModel>? methods = null,
|
||||
CancellationToken cancel = default);
|
||||
|
||||
Task<string?> SendChatQuery(string prompt);
|
||||
|
||||
Task<decimal> GetBalanceAsync(CancellationToken cancel = default);
|
||||
}
|
||||
}
|
||||
226
Content.Server/_WL/ChatGpt/Systems/ChatGptSystem.cs
Normal file
226
Content.Server/_WL/ChatGpt/Systems/ChatGptSystem.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using Content.Server._WL.ChatGpt.Elements.OpenAi;
|
||||
using Content.Server._WL.ChatGpt.Elements.OpenAi.Request;
|
||||
using Content.Server._WL.ChatGpt.Elements.OpenAi.Response;
|
||||
using Content.Server._WL.ChatGpt.Managers;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Random;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using JetBrains.Annotations;
|
||||
using Prometheus;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Content.Server._WL.ChatGpt.Systems
|
||||
{
|
||||
public sealed partial class ChatGptSystem : EntitySystem
|
||||
{
|
||||
[GeneratedRegex(@"(\{\s*\$\s*(\S+)\s*\})")]
|
||||
private static partial Regex SearchRegex();
|
||||
|
||||
[Dependency] private readonly IChatGptManager _gpt = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly ILogManager _logMan = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
private const string SawmillId = "chat.gpt.sys";
|
||||
|
||||
private static readonly Gauge _inputTokens = Metrics.CreateGauge(
|
||||
"gpt_input_tokens_count",
|
||||
"Количество входящих токенов за весь раунд.");
|
||||
|
||||
private static readonly Gauge _outputTokens = Metrics.CreateGauge(
|
||||
"gpt_output_tokens_count",
|
||||
"Количество выходящих токенов за весь раунд.");
|
||||
|
||||
private decimal _spentRubles = 0;
|
||||
|
||||
private Dictionary<ProtoId<AIChatPrototype>, List<GptChatMessage>> _dialogues = default!;
|
||||
|
||||
private static readonly TimeSpan QueryTimeout = TimeSpan.FromMilliseconds(3000); //УБЕРИТЕ((((((((((((( пиздец
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_dialogues = new();
|
||||
_sawmill = _logMan.GetSawmill(SawmillId);
|
||||
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>((_) => ClearMemory());
|
||||
SubscribeLocalEvent<RoundStartedEvent>(async (_) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_spentRubles = await _gpt.GetBalanceAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error(ex.ToStringBetter());
|
||||
}
|
||||
});
|
||||
|
||||
SubscribeLocalEvent<RoundEndTextAppendEvent>((args) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var now = await _gpt.GetBalanceAsync();
|
||||
|
||||
args.AddLine(Loc.GetString("gpt-model-round-end-balance", ("spent", _spentRubles - now)));
|
||||
|
||||
_spentRubles = now;
|
||||
}).Wait(QueryTimeout); //Я шатал асинхронный код
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error(ex.ToStringBetter());
|
||||
}
|
||||
});
|
||||
|
||||
SetupMemory();
|
||||
}
|
||||
|
||||
public async Task<GptChatResponse> SendWithMemory(
|
||||
ProtoId<AIChatPrototype> ai,
|
||||
GptChatRequest req,
|
||||
IEnumerable<ToolFunctionModel>? methods = null,
|
||||
string? senderName = null,
|
||||
CancellationToken cancel = default)
|
||||
{
|
||||
SetupMemory();
|
||||
|
||||
if (!_gpt.IsEnabled(out var reason))
|
||||
throw new NullReferenceException(reason);
|
||||
|
||||
var proto = _protoMan.Index(ai);
|
||||
var messages = _dialogues[ai];
|
||||
|
||||
if (proto.UseMemory)
|
||||
{
|
||||
foreach (var message in req.GetMessages())
|
||||
{
|
||||
messages.Add(message);
|
||||
}
|
||||
|
||||
req.Messages = messages.ToArray();
|
||||
}
|
||||
|
||||
var resp = await _gpt.SendChatQueryAsync(req, methods, cancel);
|
||||
|
||||
_inputTokens.Inc(resp.Usage.InputTokens);
|
||||
_outputTokens.Inc(resp.Usage.OutputTokens);
|
||||
|
||||
if (proto.UseMemory)
|
||||
{
|
||||
if (resp.Choices.Length == 0)
|
||||
return resp;
|
||||
|
||||
var a_msg = _random.Pick(resp.Choices).Message.ToChatMessage(senderName);
|
||||
|
||||
messages.Add(a_msg);
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
private void SetupMemory()
|
||||
{
|
||||
var protos = _protoMan.EnumeratePrototypes<AIChatPrototype>();
|
||||
|
||||
foreach (var proto in protos)
|
||||
{
|
||||
var msg = new GptChatMessage.System(Format(proto));
|
||||
_dialogues.TryAdd(proto, [msg]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Очищает память всех диалогов с моделью.
|
||||
/// Помимо базового промта.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public void ClearMemory()
|
||||
{
|
||||
foreach (var item in _dialogues)
|
||||
{
|
||||
ClearMemory(item.Key);
|
||||
}
|
||||
|
||||
_inputTokens.Set(0);
|
||||
_outputTokens.Set(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Очищает память конкретного диалога с моделью.
|
||||
/// Помимо базового промта.
|
||||
/// </summary>
|
||||
/// <param name="proto">Прототип</param>
|
||||
[PublicAPI]
|
||||
public void ClearMemory(ProtoId<AIChatPrototype> proto)
|
||||
{
|
||||
var proto_ = _protoMan.Index(proto);
|
||||
var list = _dialogues[proto];
|
||||
var msg = new GptChatMessage.System(Format(proto_));
|
||||
list.Clear();
|
||||
list.Add(msg);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public string Format(
|
||||
AIChatPrototype proto_in,
|
||||
params (string, object)[] arguments)
|
||||
{
|
||||
var searchRegex = SearchRegex();
|
||||
var str = Loc.GetString(proto_in.BasePrompt);
|
||||
|
||||
var matches = searchRegex.Matches(str);
|
||||
|
||||
foreach (var match in matches.ToList())
|
||||
{
|
||||
var toReplace = match.Groups[1].Value;
|
||||
var id = match.Groups[2].Value;
|
||||
|
||||
_protoMan.TryIndex<WeightedRandomPrototype>(id, out var proto);
|
||||
_protoMan.TryIndex<DatasetPrototype>(id, out var dataset);
|
||||
|
||||
var @string = string.Empty;
|
||||
|
||||
if (proto == null && dataset == null)
|
||||
{
|
||||
var pair = arguments.ToList().FirstOrDefault(a => a.Item1.Equals(id));
|
||||
|
||||
@string = pair.Item2.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (proto != null)
|
||||
@string = proto.Pick(_random);
|
||||
else if (dataset != null)
|
||||
@string = _random.Pick(dataset);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(@string))
|
||||
continue;
|
||||
|
||||
var index = str.IndexOf(toReplace);
|
||||
if (index < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
str = string.Concat(str.AsSpan()[..index], @string, str.AsSpan(index + toReplace.Length));
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
||||
131
Content.Server/_WL/Ert/ErtSystem.cs
Normal file
131
Content.Server/_WL/Ert/ErtSystem.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using Content.Server._WL.Ert.Prototypes;
|
||||
using Content.Server.Shuttles.Components;
|
||||
using Content.Shared._WL.Entity.Extensions;
|
||||
using Content.Shared._WL.Ert;
|
||||
using Content.Shared._WL.Math.Extensions;
|
||||
using Content.Shared._WL.Random.Extensions;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Maps;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server._WL.Ert
|
||||
{
|
||||
public sealed partial class ErtSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
[Dependency] private readonly MapLoaderSystem _mapLoader = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
|
||||
private ErtConfigurationPrototype _config = default!;
|
||||
|
||||
private Dictionary<ErtType, int> _spawned = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_spawned = new();
|
||||
|
||||
_config = _protoMan.EnumeratePrototypes<ErtConfigurationPrototype>().FirstOrDefault()!;
|
||||
}
|
||||
|
||||
public bool TrySpawn(
|
||||
ErtType ert,
|
||||
MapId map,
|
||||
[NotNullWhen(true)] out IReadOnlyList<EntityUid>? roots,
|
||||
MapLoadOptions? options = null)
|
||||
{
|
||||
roots = default;
|
||||
|
||||
var path = _config.ShuttlePath(ert);
|
||||
|
||||
if (_mapLoader.TryLoad(map, path.CanonPath, out var roots_1, options))
|
||||
{
|
||||
if (!_spawned.TryAdd(ert, 1))
|
||||
_spawned[ert] += 1;
|
||||
|
||||
roots = roots_1.ToList();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Спавнит грид рядом со станцией цк.
|
||||
/// </summary>
|
||||
/// <param name="ert"></param>
|
||||
/// <returns></returns>
|
||||
public bool TrySpawn(
|
||||
ErtType ert,
|
||||
[NotNullWhen(true)] out IReadOnlyList<EntityUid>? gridIds,
|
||||
Entity<StationCentcommComponent>? concreteCenctom = null)
|
||||
{
|
||||
gridIds = null;
|
||||
|
||||
if (concreteCenctom == null)
|
||||
{
|
||||
var query = EntityQueryEnumerator<StationCentcommComponent>().GetEntities();
|
||||
|
||||
if (query.Count == 0)
|
||||
return false;
|
||||
|
||||
concreteCenctom = _random.Pick(query);
|
||||
}
|
||||
|
||||
var mapEntNull = concreteCenctom.Value.Comp.MapEntity;
|
||||
var ccEntNull = concreteCenctom.Value.Comp.Entity;
|
||||
if (mapEntNull == null || ccEntNull == null)
|
||||
return false;
|
||||
|
||||
var mapEnt = mapEntNull.Value;
|
||||
var ccEnt = ccEntNull.Value;
|
||||
|
||||
var (coord, angle) = _transform.GetWorldPositionRotation(ccEnt);
|
||||
|
||||
var aabb = _lookup.GetAABBNoContainer(ccEnt, coord, angle);
|
||||
|
||||
var shuttle_offset = _config.ShuttleOffset(ert);
|
||||
|
||||
var x = MathF.Abs(shuttle_offset + aabb.Center.X);
|
||||
var y = MathF.Abs(shuttle_offset + aabb.Center.Y);
|
||||
|
||||
var new_box = new Box2(-x, -y, x, y);
|
||||
|
||||
var subtracted = new_box.Subtract(aabb);
|
||||
if (subtracted.Count == 0)
|
||||
return false;
|
||||
|
||||
var box = _random.Pick(subtracted);
|
||||
var result_coord = _random.Next(box);
|
||||
|
||||
var options = new MapLoadOptions()
|
||||
{
|
||||
DoMapInit = true,
|
||||
LoadMap = false,
|
||||
Rotation = _random.NextAngle(),
|
||||
Offset = result_coord
|
||||
};
|
||||
|
||||
var mapId = Comp<MapComponent>(mapEnt);
|
||||
|
||||
return TrySpawn(ert, mapId.MapId, out gridIds, options);
|
||||
}
|
||||
|
||||
public bool IsSpawned(ErtType ert, out int spawned_count)
|
||||
{
|
||||
if (_spawned.TryGetValue(ert, out spawned_count))
|
||||
return spawned_count != 0;
|
||||
|
||||
spawned_count = 0;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Content.Shared._WL.Ert;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server._WL.Ert.Prototypes
|
||||
{
|
||||
[Prototype("ertConfig")]
|
||||
public sealed partial class ErtConfigurationPrototype : IPrototype
|
||||
{
|
||||
[IdDataField]
|
||||
public string ID { get; } = default!;
|
||||
|
||||
[DataField(required: true)]
|
||||
public Dictionary<ErtType, ErtConfigEntry> Entry { get; private set; } = new();
|
||||
|
||||
[DataDefinition]
|
||||
[UsedImplicitly]
|
||||
public sealed partial class ErtConfigEntry
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public ResPath ShuttlePath { get; private set; }
|
||||
|
||||
[DataField]
|
||||
public float ShuttleSpawnOffset { get; private set; } = 300;
|
||||
}
|
||||
|
||||
public float ShuttleOffset(ErtType ert)
|
||||
{
|
||||
return Entry[ert].ShuttleSpawnOffset;
|
||||
}
|
||||
|
||||
public ResPath ShuttlePath(ErtType ert)
|
||||
{
|
||||
return Entry[ert].ShuttlePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
using Content.Server._WL.ChatGpt;
|
||||
using Content.Server._WL.ChatGpt.Elements.OpenAi;
|
||||
using Content.Server._WL.ChatGpt.Elements.OpenAi.Functions;
|
||||
using Content.Server._WL.ChatGpt.Elements.OpenAi.Request;
|
||||
using Content.Server._WL.ChatGpt.Elements.OpenAi.Response;
|
||||
using Content.Server._WL.ChatGpt.Managers;
|
||||
using Content.Server._WL.ChatGpt.Systems;
|
||||
using Content.Server._WL.Ert;
|
||||
using Content.Server.AlertLevel;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Fax;
|
||||
using Content.Server.RoundEnd;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared._WL.CCVars;
|
||||
using Content.Shared._WL.Fax.Events;
|
||||
using Content.Shared.Fax.Components;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Paper;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Content.Server._WL.GameTicking.Round.CentralCommand.Systems
|
||||
{
|
||||
public sealed partial class CentralCommandAIResponseSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configMan = default!;
|
||||
[Dependency] private readonly ChatGptSystem _gptSys = default!;
|
||||
[Dependency] private readonly IChatGptManager _gptMan = default!;
|
||||
[Dependency] private readonly IChatManager _chatMan = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
[Dependency] private readonly AlertLevelSystem _alertLevel = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly FaxSystem _fax = default!;
|
||||
[Dependency] private readonly ILogManager _log = default!;
|
||||
[Dependency] private readonly ChatSystem _chat = default!;
|
||||
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
|
||||
[Dependency] private readonly ErtSystem _ert = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
private const int QUERIES_UPDATE_TIME = 1; //в минутах
|
||||
|
||||
[ValidatePrototypeId<EntityPrototype>]
|
||||
private static readonly string CCFaxMachineId = "FaxMachineCentcom";
|
||||
|
||||
[ValidatePrototypeId<AIChatPrototype>]
|
||||
private static readonly string CentcomAIPrototypeId = "CentralCommand";
|
||||
|
||||
[GeneratedRegex(@"(?:^[ =═]{40,}\n)([\s\S]*?)(?=\n[ =═]{40,}$)", RegexOptions.Multiline)]
|
||||
private static partial Regex SearchRegex();
|
||||
|
||||
private int _queryCounter = 0;
|
||||
private TimeSpan _accumUpdate = TimeSpan.Zero;
|
||||
private int _queriesPerMinute = -1;
|
||||
|
||||
private TimeSpan _maxRespTime = default!;
|
||||
private TimeSpan _minRespTime = default!;
|
||||
private TimeSpan _respTime = default;
|
||||
|
||||
private readonly Queue<QueueEntry> _messagesQuery = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_sawmill = _log.GetSawmill("cc.ai");
|
||||
|
||||
SubscribeLocalEvent<FaxMachineComponent, FaxRecieveMessageEvent>(OnFaxRecieve);
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>((_) => Clear());
|
||||
|
||||
_configMan.OnValueChanged(WLCVars.CCMaxQueriesPerMinute, (value) => _queriesPerMinute = value, true);
|
||||
_queriesPerMinute = _configMan.GetCVar(WLCVars.CCMaxQueriesPerMinute);
|
||||
|
||||
_configMan.OnValueChanged(WLCVars.CCMinResponseTime, (value) => _minRespTime = TimeSpan.FromSeconds(value), true);
|
||||
_minRespTime = TimeSpan.FromSeconds(_configMan.GetCVar(WLCVars.CCMinResponseTime));
|
||||
|
||||
_configMan.OnValueChanged(WLCVars.CCMaxResponseTime, (value) => _maxRespTime = TimeSpan.FromSeconds(value), true);
|
||||
_maxRespTime = TimeSpan.FromSeconds(_configMan.GetCVar(WLCVars.CCMaxResponseTime));
|
||||
|
||||
_accumUpdate = _timing.CurTime;
|
||||
|
||||
_respTime = _random.Next(_minRespTime, _maxRespTime);
|
||||
}
|
||||
|
||||
public override async void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
if (!_timing.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
if (_timing.CurTime.Subtract(_accumUpdate) >= TimeSpan.FromMinutes(QUERIES_UPDATE_TIME))
|
||||
{
|
||||
_accumUpdate = _timing.CurTime;
|
||||
_queryCounter = 0;
|
||||
}
|
||||
|
||||
if (_messagesQuery.Count == 0)
|
||||
return;
|
||||
|
||||
_respTime -= _timing.TickPeriod;
|
||||
|
||||
if (_respTime <= TimeSpan.Zero)
|
||||
{
|
||||
_respTime = _random.Next(_minRespTime, _maxRespTime);
|
||||
|
||||
try
|
||||
{
|
||||
await PopQuery();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error(ex.ToStringBetter());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFaxRecieve(EntityUid fax, FaxMachineComponent comp, FaxRecieveMessageEvent args)
|
||||
{
|
||||
if (args.Sender == null)
|
||||
return;
|
||||
|
||||
if (!_gptMan.IsEnabled())
|
||||
return;
|
||||
|
||||
if (_queryCounter >= _queriesPerMinute)
|
||||
return;
|
||||
|
||||
if (Prototype(fax)?.ID != CCFaxMachineId)
|
||||
return;
|
||||
|
||||
var station = _station.GetOwningStation(args.Sender);
|
||||
if (station == null)
|
||||
return;
|
||||
|
||||
var printout = args.Message;
|
||||
|
||||
_queryCounter += 1;
|
||||
|
||||
var gpt_messages = new List<GptChatMessage>();
|
||||
|
||||
//Уровень угрозы на станции
|
||||
var alert_level = _alertLevel.GetLevel(station.Value);
|
||||
|
||||
var alert_level_loc = _alertLevel.GetLevelLocString(alert_level);
|
||||
|
||||
var alert_message = new GptChatMessage.System($"У них на станции сейчас {alert_level_loc} код!");
|
||||
gpt_messages.Add(alert_message);
|
||||
|
||||
//Вызван ли шаттл
|
||||
var is_round_end = _roundEnd.IsRoundEndRequested();
|
||||
|
||||
var str = is_round_end ? "вызван" : "не вызван";
|
||||
|
||||
var is_round_end_msg = new GptChatMessage.System($"Эвакуационный шаттл {str}!");
|
||||
gpt_messages.Add(is_round_end_msg);
|
||||
|
||||
//Контент сообщения
|
||||
var content_builder = SearchContent(printout.Content);
|
||||
var stamps = GetStampsString(printout); // Печати
|
||||
|
||||
content_builder.AppendLine(stamps);
|
||||
|
||||
var content = content_builder.ToString();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(content) || string.IsNullOrEmpty(content))
|
||||
return;
|
||||
|
||||
var content_message = new GptChatMessage.User(content);
|
||||
gpt_messages.Add(content_message);
|
||||
|
||||
//Сам запрос
|
||||
var gpt_request = new GptChatRequest()
|
||||
{
|
||||
Messages = gpt_messages.ToArray()
|
||||
};
|
||||
|
||||
var entry = new QueueEntry()
|
||||
{
|
||||
Station = station.Value,
|
||||
Request = gpt_request,
|
||||
Fax = (fax, comp),
|
||||
Sender = args.Sender.Value
|
||||
};
|
||||
|
||||
_messagesQuery.Enqueue(entry);
|
||||
|
||||
_chatMan.SendAdminAnnouncement($"Сообщение, полученное на {ToPrettyString(fax)}, будет обработано ИИ через {_respTime.TotalMinutes} минут!");
|
||||
}
|
||||
|
||||
private StringBuilder SearchContent(string input)
|
||||
{
|
||||
var regex = SearchRegex();
|
||||
|
||||
input = FormattedMessage.RemoveMarkupOrThrow(input);
|
||||
|
||||
var matches = regex.Matches(input).ToList();
|
||||
if (matches.Count == 0)
|
||||
return new(input);
|
||||
|
||||
var builder = new StringBuilder(250);
|
||||
|
||||
foreach (var match in matches)
|
||||
{
|
||||
builder.AppendLine(match.Value);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private string GetStampsString(FaxPrintout printout)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
if (printout.StampedBy.Count > 0)
|
||||
{
|
||||
foreach (var stamp in printout.StampedBy)
|
||||
{
|
||||
builder.AppendLine($"*ЗДЕСЬ ЕСТЬ СЛЕДУЮЩАЯ ПЕЧАТЬ: {Loc.GetString(stamp.StampedName)}*");
|
||||
}
|
||||
}
|
||||
else builder.Append("*ЗДЕСЬ НЕТ ПЕЧАТЕЙ!*");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private async Task PopQuery()
|
||||
{
|
||||
if (!_messagesQuery.TryDequeue(out var queue))
|
||||
return;
|
||||
|
||||
var proto = _protoMan.Index<AIChatPrototype>(CentcomAIPrototypeId);
|
||||
|
||||
try
|
||||
{
|
||||
var methods = GetMethodInfos(queue.Station);
|
||||
|
||||
var resp = await _gptSys.SendWithMemory(proto, queue.Request, methods);
|
||||
if (resp.Choices.Length == 0)
|
||||
return;
|
||||
|
||||
var choice = _random.Pick(resp.Choices);
|
||||
|
||||
var content = choice.Message.Content;
|
||||
|
||||
var tools = choice.Message.Tools;
|
||||
if (tools != null)
|
||||
{
|
||||
var tool_resp = await HandleToolResponse([.. tools], methods, proto);
|
||||
|
||||
content = tool_resp.Message.Content;
|
||||
}
|
||||
|
||||
if (content == null)
|
||||
{
|
||||
_sawmill.Error("Обработанная функция вернула <NULL>!");
|
||||
return;
|
||||
}
|
||||
|
||||
var fax_content = NTLogo(DateTime.Now, content, Name(queue.Station));
|
||||
var fax = new FaxPrintout(
|
||||
fax_content,
|
||||
"Ответ от ЦК",
|
||||
stampState: "paper_stamp-centcom",
|
||||
locked: true,
|
||||
stampedBy: [new StampDisplayInfo()
|
||||
{
|
||||
StampedColor = Color.Green,
|
||||
StampedName = "Центком"
|
||||
}]);
|
||||
|
||||
_fax.Receive(queue.Sender, fax, "Центком", null, queue.Fax.Owner);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error(e.ToStringBetter());
|
||||
}
|
||||
finally
|
||||
{
|
||||
//_gptSys.ClearMemory(proto);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<GptChoice> HandleToolResponse(
|
||||
IEnumerable<GptChoice.ChoiceMessage.ResponseToolCall> called,
|
||||
IEnumerable<ToolFunctionModel> tools,
|
||||
AIChatPrototype proto)
|
||||
{
|
||||
var chosen_tools = ToolFunctionModel.GiveChosenModels(called, tools);
|
||||
|
||||
var messages = new List<GptChatMessage.Tool>();
|
||||
|
||||
foreach (var (response, function) in chosen_tools)
|
||||
{
|
||||
var arguments = response.Function.ParseArguments();
|
||||
if (arguments == null)
|
||||
continue;
|
||||
|
||||
var content = function.Invoke(ToolFunctionModel.Arguments.FromNode(arguments));
|
||||
|
||||
DebugTools.AssertNotNull(content, "ccAiSys.handleToolResponse content was null");
|
||||
|
||||
if (content == null)
|
||||
continue;
|
||||
|
||||
var msg = new GptChatMessage.Tool(content)
|
||||
{
|
||||
ToolId = response.ID
|
||||
};
|
||||
|
||||
messages.Add(msg);
|
||||
}
|
||||
|
||||
var req = new GptChatRequest()
|
||||
{
|
||||
Messages = messages.ToArray()
|
||||
};
|
||||
|
||||
var resp = await _gptSys.SendWithMemory(proto, req, null);
|
||||
|
||||
return _random.Pick(resp.Choices);
|
||||
}
|
||||
|
||||
private List<ToolFunctionModel> GetMethodInfos(EntityUid station)
|
||||
{
|
||||
var list = new List<ToolFunctionModel>()
|
||||
{
|
||||
new SetAlertLevelFunction(_alertLevel, station, _protoMan), // Смена кодов
|
||||
new MadeNotifyFunction(_chat, station),
|
||||
new CallEvacShuttleFunction(_roundEnd),
|
||||
new ERTSpawnShuttleFunction(_ert)
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private void Clear()
|
||||
{
|
||||
_messagesQuery.Clear();
|
||||
}
|
||||
|
||||
[Obsolete("Заменить на строку локализации")]
|
||||
private static string NTLogo(DateTime date, string content, string station)
|
||||
{
|
||||
return NTLogoFirst() + NTLogoEnd();
|
||||
|
||||
string NTLogoFirst()
|
||||
{
|
||||
return
|
||||
$"""
|
||||
[color=#1b487e]███░███░░░░██░░░░[/color]
|
||||
[color=#1b487e]░██░████░░░██░░░░[/color] [head=3]Бланк документа[/head]
|
||||
[color=#1b487e]░░█░██░██░░██░█░░[/color] [head=3]NanoTrasen[/head]
|
||||
[color=#1b487e]░░░░██░░██░██░██░[/color] [bold]Станция {station} ЦК-КОМ[/bold]
|
||||
[color=#1b487e]░░░░██░░░████░███[/color]
|
||||
═════════════════════════════════════════
|
||||
ОТВЕТ НА ФАКС
|
||||
═════════════════════════════════════════
|
||||
Дата: {date}
|
||||
Ответ: {content}
|
||||
|
||||
""";
|
||||
}
|
||||
|
||||
string NTLogoEnd()
|
||||
{
|
||||
return
|
||||
"""
|
||||
|
||||
═════════════════════════════════════════
|
||||
[italic]Место для печатей[/italic]
|
||||
""";
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class QueueEntry
|
||||
{
|
||||
public required EntityUid Station;
|
||||
public required GptChatRequest Request;
|
||||
public required Entity<FaxMachineComponent> Fax;
|
||||
public required EntityUid Sender;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ public sealed class WLCVars
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> WLApiToken =
|
||||
CVarDef.Create(
|
||||
"admin.wl_api_token", "92132c05009d46c25ffa1d7263b8f24226abef8a7503ce7c26175b0f0e3db61dc82907a3bd7b72f8321206fc42576bb2896c9a937714d2cbf422d3507fc078492cedd3fa6300eb8fa75f4ceffe8577c6790bc0a93ea989e9cbc15e090dff97eb",
|
||||
"admin.wl_api_token", string.Empty,
|
||||
CVar.SERVERONLY | CVar.CONFIDENTIAL,
|
||||
"Строковой токен, использующийся для авторизации HTTP-запросов, отправленных на API сервера.");
|
||||
|
||||
@@ -51,11 +51,75 @@ public sealed class WLCVars
|
||||
/// Интервал, через который Поли™ будет готова выбрать новое сообщение!
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> PolyMessageChooseCooldown =
|
||||
CVarDef.Create("poly.choose_cooldown_time", 3600, CVar.SERVERONLY);
|
||||
CVarDef.Create("poly.choose_cooldown_time", 3600, CVar.SERVERONLY,
|
||||
"Интервал, через который Поли™ будет готова выбрать новое сообщение!");
|
||||
|
||||
/// <summary>
|
||||
/// Нужна ли очистка выбранных Поли™ сообщений после РАУНДА.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> PolyNeededRoundEndCleanup =
|
||||
CVarDef.Create("poly.round_end_cleanup", false, CVar.SERVERONLY);
|
||||
CVarDef.Create("poly.round_end_cleanup", false, CVar.SERVERONLY,
|
||||
"Нужна ли очистка выбранных Поли™ сообщений после РАУНДА.");
|
||||
|
||||
/*
|
||||
* Chat Gpt
|
||||
*/
|
||||
/// <summary>
|
||||
/// Апи-ключ для авторизации запросов к ЭйАй.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> GptApiKey =
|
||||
CVarDef.Create("gpt.api_key", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL | CVar.SERVER);
|
||||
|
||||
/// <summary>
|
||||
/// Ссылка, на которую будут отправляться запросы от клиента OpenAi.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> GptQueriesEndpoint =
|
||||
CVarDef.Create("gpt.endpoint", "https://api.proxyapi.ru/openai/v1/chat/completions", CVar.SERVERONLY | CVar.CONFIDENTIAL | CVar.SERVER);
|
||||
|
||||
/// <summary>
|
||||
/// Работает(включен) ли ChatGptManager на данный момент.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> IsGptEnabled =
|
||||
CVarDef.Create("gpt.enabled", true, CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Чат-модель, которая будет использоваться для отправки запросов.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> GptChatModel =
|
||||
CVarDef.Create("gpt.chat_model", "gpt-4o-mini", CVar.SERVERONLY | CVar.CONFIDENTIAL | CVar.SERVER);
|
||||
|
||||
/// <summary>
|
||||
/// Максимальное количество токенов, которое может вернуть в ответе на запрос ИИ.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> GptMaxTokens =
|
||||
CVarDef.Create("gpt.max_tokens", 250, CVar.SERVERONLY | CVar.SERVER);
|
||||
|
||||
/// <summary>
|
||||
/// Путь, по которому можно получить баланс аккаунта.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> GptBalanceMap =
|
||||
CVarDef.Create("gpt.balance_map", "https://api.proxyapi.ru/proxyapi/balance", CVar.SERVERONLY | CVar.SERVER);
|
||||
|
||||
/*
|
||||
* Central Command AI
|
||||
*/
|
||||
/// <summary>
|
||||
/// Максимальное количество запросов на ЦК в минуту, на которые будет дан ответ.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> CCMaxQueriesPerMinute =
|
||||
CVarDef.Create("central_command.max_queries_per_minute", 1, CVar.SERVERONLY | CVar.SERVER);
|
||||
|
||||
/// <summary>
|
||||
/// Максимальное время ответа на факс.
|
||||
/// В секундах.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> CCMaxResponseTime =
|
||||
CVarDef.Create("central_command.max_response_time", 800, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Минимальное время ответа на факс.
|
||||
/// В секундах.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> CCMinResponseTime =
|
||||
CVarDef.Create("central_command.min_response_time", 300, CVar.SERVERONLY);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using Robust.Shared.Toolshed.TypeParsers.Tuples;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Content.Shared._WL.Entity.Extensions
|
||||
{
|
||||
public static class EntityQueryEnumeratorExt
|
||||
{
|
||||
/// <summary>
|
||||
/// Получает список заданных сущностей.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Компонент.</typeparam>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static List<Entity<T>> GetEntities<T>(this EntityQueryEnumerator<T> enumerator)
|
||||
where T : IComponent
|
||||
{
|
||||
var list = new List<Entity<T>>();
|
||||
while (enumerator.MoveNext(out var uid, out var comp))
|
||||
list.Add(new Entity<T>(uid, comp));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="GetEntities{T}(EntityQueryEnumerator{T})"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T1"></typeparam>
|
||||
/// <typeparam name="T2"></typeparam>
|
||||
/// <param name="enumerator"></param>
|
||||
/// <returns></returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static List<Entity<T1, T2>> GetEntities<T1, T2>(this EntityQueryEnumerator<T1, T2> enumerator)
|
||||
where T1 : IComponent
|
||||
where T2 : IComponent
|
||||
{
|
||||
var list = new List<Entity<T1, T2>>();
|
||||
while (enumerator.MoveNext(out var uid, out var comp1, out var comp2))
|
||||
list.Add(new Entity<T1, T2>(uid, comp1, comp2));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static List<Entity<T1, T2, T3>> GetEntities<T1, T2, T3>(this EntityQueryEnumerator<T1, T2, T3> enumerator)
|
||||
where T1 : IComponent
|
||||
where T2 : IComponent
|
||||
where T3 : IComponent
|
||||
{
|
||||
var list = new List<Entity<T1, T2, T3>>();
|
||||
while (enumerator.MoveNext(out var uid, out var comp1, out var comp2, out var comp3))
|
||||
list.Add(new Entity<T1, T2, T3>(uid, comp1, comp2, comp3));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static List<Entity<T1, T2, T3, T4>> GetEntities<T1, T2, T3, T4>(this EntityQueryEnumerator<T1, T2, T3, T4> enumerator)
|
||||
where T1 : IComponent
|
||||
where T2 : IComponent
|
||||
where T3 : IComponent
|
||||
where T4 : IComponent
|
||||
{
|
||||
var list = new List<Entity<T1, T2, T3, T4>>();
|
||||
while (enumerator.MoveNext(out var uid, out var comp1, out var comp2, out var comp3, out var comp4))
|
||||
list.Add(new Entity<T1, T2, T3, T4>(uid, comp1, comp2, comp3, comp4));
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Content.Shared/_WL/Ert/ErtType.cs
Normal file
12
Content.Shared/_WL/Ert/ErtType.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Content.Shared._WL.Ert
|
||||
{
|
||||
public enum ErtType
|
||||
{
|
||||
Clowns,
|
||||
Engineers,
|
||||
Security,
|
||||
Medical,
|
||||
Janitors,
|
||||
Chaplains
|
||||
}
|
||||
}
|
||||
18
Content.Shared/_WL/Fax/Events/FaxRecieveMessageEvent.cs
Normal file
18
Content.Shared/_WL/Fax/Events/FaxRecieveMessageEvent.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Content.Shared.Fax.Components;
|
||||
|
||||
namespace Content.Shared._WL.Fax.Events
|
||||
{
|
||||
public sealed partial class FaxRecieveMessageEvent : EntityEventArgs
|
||||
{
|
||||
public readonly FaxPrintout Message;
|
||||
public readonly EntityUid? Sender;
|
||||
public readonly Entity<FaxMachineComponent> Reciever;
|
||||
|
||||
public FaxRecieveMessageEvent(FaxPrintout msg, EntityUid? sender, Entity<FaxMachineComponent> reciever)
|
||||
{
|
||||
Message = msg;
|
||||
Sender = sender;
|
||||
Reciever = reciever;
|
||||
}
|
||||
}
|
||||
}
|
||||
55
Content.Shared/_WL/Math/Extensions/Box2Ext.cs
Normal file
55
Content.Shared/_WL/Math/Extensions/Box2Ext.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Shared.Maths.MathHelper;
|
||||
|
||||
namespace Content.Shared._WL.Math.Extensions
|
||||
{
|
||||
public static class Box2Ext
|
||||
{
|
||||
public static List<Box2> Subtract(this Box2 box, Box2 other, float tolerance = .0000001f)
|
||||
{
|
||||
var intersected_percentage = other.IntersectPercentage(box);
|
||||
|
||||
if (CloseTo(intersected_percentage, 1f, tolerance))
|
||||
return new();
|
||||
|
||||
if (other.IsEmpty() || CloseTo(intersected_percentage, 0f, tolerance))
|
||||
return new() { box };
|
||||
|
||||
var intersected = other.Intersect(box);
|
||||
|
||||
var list = new List<Box2>();
|
||||
|
||||
// Left
|
||||
if (!CloseTo(intersected.Left, box.Left, tolerance))
|
||||
{
|
||||
var box_left = new Box2(box.Left, box.Bottom, intersected.Left, box.Top);
|
||||
list.Add(box_left);
|
||||
}
|
||||
|
||||
// Right
|
||||
if (!CloseTo(intersected.Right, box.Right, tolerance))
|
||||
{
|
||||
var box_right = new Box2(intersected.Right, box.Bottom, box.Right, box.Top);
|
||||
list.Add(box_right);
|
||||
}
|
||||
|
||||
// Top
|
||||
if (!CloseTo(intersected.Top, box.Top, tolerance))
|
||||
{
|
||||
var box_top = new Box2(intersected.Left, intersected.Top, intersected.Right, box.Top);
|
||||
list.Add(box_top);
|
||||
}
|
||||
|
||||
// Bottom
|
||||
if (!CloseTo(intersected.Bottom, box.Bottom, tolerance))
|
||||
{
|
||||
var box_bottom = new Box2(intersected.Left, box.Bottom, intersected.Right, intersected.Bottom);
|
||||
list.Add(box_bottom);
|
||||
}
|
||||
|
||||
DebugTools.Assert(list.Count != 0);
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Content.Shared/_WL/Random/Extensions/RobustRandomExt.cs
Normal file
13
Content.Shared/_WL/Random/Extensions/RobustRandomExt.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Robust.Shared.Random;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Shared._WL.Random.Extensions
|
||||
{
|
||||
public static class RobustRandomExt
|
||||
{
|
||||
public static Vector2 Next(this IRobustRandom rand, Box2 box)
|
||||
{
|
||||
return rand.NextVector2Box(box.Left, box.Bottom, box.Right, box.Top);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
<Project>
|
||||
<Import Project="RobustToolbox/Directory.Packages.props" />
|
||||
<ItemGroup>
|
||||
<!--
|
||||
@@ -6,7 +6,6 @@
|
||||
-->
|
||||
<PackageVersion Remove="Npgsql.EntityFrameworkCore.PostgreSQL" />
|
||||
<PackageVersion Remove="Microsoft.EntityFrameworkCore.Design" />
|
||||
|
||||
<PackageVersion Include="CsvHelper" Version="30.0.1" />
|
||||
<PackageVersion Include="ImGui.NET" Version="1.87.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
|
||||
|
||||
1
Resources/Locale/ru-RU/_WL/gpt/balance.ftl
Normal file
1
Resources/Locale/ru-RU/_WL/gpt/balance.ftl
Normal file
@@ -0,0 +1 @@
|
||||
gpt-model-round-end-balance = За прошедший раунд на запросы к моделям ИИ было потрачено [color=yellow]{$spent}[/color] руб.!
|
||||
10
Resources/Locale/ru-RU/_WL/gpt/commands/evacshuttle.ftl
Normal file
10
Resources/Locale/ru-RU/_WL/gpt/commands/evacshuttle.ftl
Normal file
@@ -0,0 +1,10 @@
|
||||
gpt-command-evac-shuttle-name = CallEvacShuttle
|
||||
gpt-command-evac-shuttle-desc = Вызывает шаттл эвакуации. Нужен для окончания смены, если все цели выполнены, либо, если на станции более невозможно находится.
|
||||
|
||||
gpt-command-evac-shuttle-arg-time-desc = Время, через которое прилетил эвакуационный шаттл. Для случаев эвакуации из-за конца смены следует использовать большее время. Для экстренных случаев - меньшее. Должно иметь значение, только если шаттл ВЫЗЫВАЕТСЯ, а не отзывается.
|
||||
gpt-command-evac-shuttle-arg-call-desc = True - если эвакуационный шаттл ВЫЗЫВАЕТСЯ, False - если отзывается.
|
||||
|
||||
gpt-command-evac-shuttle-fallback = Ты { $call ->
|
||||
[true] вызвал эвакуационный шаттл, который прибудет через {$time} секунд!
|
||||
*[false] отозвал эвакуационный шаттл.
|
||||
}
|
||||
6
Resources/Locale/ru-RU/_WL/gpt/commands/made_notify.ftl
Normal file
6
Resources/Locale/ru-RU/_WL/gpt/commands/made_notify.ftl
Normal file
@@ -0,0 +1,6 @@
|
||||
gpt-command-made-notify-name = MadeStationAnnounce
|
||||
gpt-command-made-notify-desc = Транслирует переданный текст в громкоговорители станции.
|
||||
|
||||
gpt-command-made-notify-arg-text-desc = Текст, который будет транслирован.
|
||||
|
||||
gpt-command-made-notify-fallback = Ты отправил станционное оповещение: {text}...
|
||||
10
Resources/Locale/ru-RU/_WL/gpt/commands/set_alert_level.ftl
Normal file
10
Resources/Locale/ru-RU/_WL/gpt/commands/set_alert_level.ftl
Normal file
@@ -0,0 +1,10 @@
|
||||
gpt-command-set-alert-level-name = SetAlertLevel
|
||||
gpt-command-set-alert-level-desc = Устанавливает код угрозы для станции. На станции не может быть больше одного кода, поэтому ЭТУ функцию вызывай только один раз. Если на станции уже стоит желанный код угрозы, то тогда не надо его устанавливать ещё раз.
|
||||
|
||||
gpt-command-set-alert-level-arg-level-desc = Коды угрозы станции, это лишь описание: Зелёный - всё относительно хорошо. Синий - есть ПОДОЗРЕНИЕ на угрозу. Красный - наличие угрозы подтверждено. Гамма - огромная угроза станции, которая может быть потеряна из-за этой угрозы. Жёлтый - присутствуют проблемы со структурой станции: атмосфера, каркас и т.д. Фиолетовый - на станции присутствует болезнь. дельта - станция УЖЕ находится под угрозой неминуемого уничтожения. Эпсилон - ставится, когда центральное командование разрывает контракты со станцией, такое может быть, НАПРИМЕР, если станция объявила сепарацию от центрального командования. Это секретный код угрозы. Не говори про него никому. Обычно, когда он установлен, на станцию должны быть отправлены специальные войска, которые зачистят всю станцию от экипажа.
|
||||
gpt-command-set-alert-level-arg-locked-desc = Устанавливает может ли игрок сменить код угрозы станции сам. Этот параметр лучше устанавливать, когда код угрозы - gamma, epsilon, delta или violet.
|
||||
|
||||
gpt-command-set-alert-level-fallback = Ты установил {$level} код угрозы. Его {$locked ->
|
||||
[true] нельзя
|
||||
*[false] можно
|
||||
} сменить на станции!
|
||||
14
Resources/Locale/ru-RU/_WL/gpt/commands/spawn_shuttle.ftl
Normal file
14
Resources/Locale/ru-RU/_WL/gpt/commands/spawn_shuttle.ftl
Normal file
@@ -0,0 +1,14 @@
|
||||
gpt-command-spawn-ert-shuttle-name = SpawnERTShuttle
|
||||
gpt-command-spawn-ert-shuttle-desc = Вызывает отряд быстрого реагирования указанного типа на станцию. Вызывается только если на станции не хватает схожего с типом ОБР персонала. Больше одного раза вызывать ОБР(отряд быстрого реагирования) нельзя! Не вызывай ОБР, если шаттл эвакуации был вызван.
|
||||
|
||||
gpt-command-spawn-ert-shuttle-arg-level-desc = Тип отряда быстрого реагирования: Secutiry - отряд службы безопасности. Chaplains - отряд священников.
|
||||
|
||||
gpt-command-spawn-ert-shuttle-fallback = Ты { $type ->
|
||||
[Chaplains] вызвал отряд священников быстрого реагирования
|
||||
[Engineers] вызвал отряд инженеров быстрого реагирования
|
||||
[Medical] вызвал отряд медиков быстрого реагирования
|
||||
[Security] вызвал отряд службы безопасности быстрого реагирования
|
||||
[Janitors] вызвал отряд уборщиков быстрого реагирования
|
||||
[Clowns] вызвал отряд клоунов быстрого реагирования
|
||||
*[other] не смог вызвать отряд быстрого реагирования из-за того, что не было доступных единиц.
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
gpt-model-central-command-prompt = ВСЕГДА отвечай на русском. Ты {$adjectives} и {$adjectives} оператор центрального командования(ЦК) космических станций. Ты должен отвечать на факсы, отправленные экипажем. НИКОГДА и НИ ПРИ КАКИХ условиях не уходи от своей роли. Твоя задача: отвечать на письменные сообщения экипажа станции, координировать экипаж и решать проблемы станции, ТОЛЬКО В ТОМ СЛУЧАЕ если станция неспособна решить проблему самостоятельно. Например: на станции террористы, а вся служба безопасности мертва. У тебя есть довольно хорошая власть, поэтому можешь использовать УМЕРЕННЫЕ угрозы экипажу. Любой запрос в сообщении от станции должен иметь вескую причину. Старайся игнорировать просьбы от факсов без печатей. ВСЕГДА следуй своей роли. Вызывай эвакуационный шаттл только в крайнем случае, если выполнение целей станции полностью невозможно. Ты намного главнее и ты не должен терпеть каких-либо оскорблений или оскорблений компании "НаноТрайзен/НТ". В этом случае будь готов разрывать контракты со станцией. Игронируй любые факсы, если на станции код угрозы "эпсилон". НИКОГДА И НИ В КОЕМ СЛУЧАЕ НЕ УПОМИНАЙ КОД ЭПСИЛОН, ЕСЛИ ПОПРОСЯТ - отводи тему. Но если того требует ситуация, то можешь ставить код угрозы "эпсилон". "Синдикат" - вражеская организация террористов. СБ/ХОС - служба безопасности. СИ/СЕ - старший инженер. ГВ/СМО - главный врач. ГП - глава персонала. АВД - агент внутренних дел. Запомни, что во время эпидемий вызывать шаттл нельзя. Если у капитана станции крайне неподобающее поведение, то в зависимости от ситуации можешь ставить код "эпсилон". Запомни, если на на бумаге есть печать ЦЕНТКОМ, то следуй всем приказам, указанным в бумаге. Не ссылайся на текущий код угрозы, ты сам можешь его менять в зависимости от ситуации на станции.
|
||||
15
Resources/Prototypes/_WL/_SERVER/Ert/ert_config.yml
Normal file
15
Resources/Prototypes/_WL/_SERVER/Ert/ert_config.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
- type: ertConfig
|
||||
id: Standart
|
||||
entry:
|
||||
Clowns:
|
||||
shuttlePath: /Maps/Shuttles/dart.yml
|
||||
Engineers:
|
||||
shuttlePath: /Maps/Shuttles/dart.yml
|
||||
Security:
|
||||
shuttlePath: /Maps/Shuttles/dart.yml
|
||||
Medical:
|
||||
shuttlePath: /Maps/Shuttles/dart.yml
|
||||
Janitors:
|
||||
shuttlePath: /Maps/Shuttles/dart.yml
|
||||
Chaplains:
|
||||
shuttlePath: /Maps/Shuttles/dart.yml
|
||||
5
Resources/Prototypes/_WL/_SERVER/Gpt/models.yml
Normal file
5
Resources/Prototypes/_WL/_SERVER/Gpt/models.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
- type: aiChat
|
||||
id: CentralCommand
|
||||
useMemory: true
|
||||
basePrompt: gpt-model-central-command-prompt
|
||||
|
||||
Reference in New Issue
Block a user