Compare commits
5 commits
c3f3cc6acb
...
3135428fb2
Author | SHA1 | Date | |
---|---|---|---|
3135428fb2 | |||
4c1da1fa7d | |||
067a5365d2 | |||
b9c218111a | |||
7b322ef7fb |
17 changed files with 268 additions and 9 deletions
24
LICENSE
Normal file
24
LICENSE
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
Copyright (c) 2023 femsci
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
1. Any person intending to use the Software is required to pet a cat at least
|
||||||
|
twice within the month preceding the use of the Software.
|
||||||
|
|
||||||
|
2. Any person intending to use the Software must endear feline species with extraordinary affection.
|
||||||
|
|
||||||
|
3. The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
10
src/Nyanlabs.Umogen.Core/Models/ApiChoice.cs
Normal file
10
src/Nyanlabs.Umogen.Core/Models/ApiChoice.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
namespace Nyanlabs.Umogen.Core.Models;
|
||||||
|
|
||||||
|
public class ApiChoice
|
||||||
|
{
|
||||||
|
public string FinishReason { get; set; } = default!;
|
||||||
|
public int Index { get; set; }
|
||||||
|
public GptMessage? Message { get; set; }
|
||||||
|
public GptMessage? Delta { get; set; }
|
||||||
|
|
||||||
|
}
|
|
@ -4,4 +4,7 @@ public record ApiRequest
|
||||||
{
|
{
|
||||||
public string Model { get; set; } = "gpt-3.5-turbo";
|
public string Model { get; set; } = "gpt-3.5-turbo";
|
||||||
public float Temperature { get; set; } = 0.7f;
|
public float Temperature { get; set; } = 0.7f;
|
||||||
|
public ICollection<GptMessage> Messages { get; set; } = new List<GptMessage>();
|
||||||
|
public bool Stream { get; set; }
|
||||||
|
//public ApiResponseFormat ResponseFormat { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
11
src/Nyanlabs.Umogen.Core/Models/ApiResponse.cs
Normal file
11
src/Nyanlabs.Umogen.Core/Models/ApiResponse.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
namespace Nyanlabs.Umogen.Core.Models;
|
||||||
|
|
||||||
|
public class ApiResponse
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = default!;
|
||||||
|
public string Model { get; set; } = default!;
|
||||||
|
public string Object { get; set; } = default!;
|
||||||
|
public UsageStats Usage { get; set; } = default!;
|
||||||
|
public ICollection<ApiChoice> Choices { get; set; } = new List<ApiChoice>();
|
||||||
|
public long Created { get; set; }
|
||||||
|
}
|
6
src/Nyanlabs.Umogen.Core/Models/ApiResponseFormat.cs
Normal file
6
src/Nyanlabs.Umogen.Core/Models/ApiResponseFormat.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
namespace Nyanlabs.Umogen.Core.Models;
|
||||||
|
|
||||||
|
public class ApiResponseFormat
|
||||||
|
{
|
||||||
|
public string Type { get; set; } = "json_object";
|
||||||
|
}
|
7
src/Nyanlabs.Umogen.Core/Models/GptMessage.cs
Normal file
7
src/Nyanlabs.Umogen.Core/Models/GptMessage.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Nyanlabs.Umogen.Core.Models;
|
||||||
|
|
||||||
|
public record GptMessage(string Role, string Content)
|
||||||
|
{
|
||||||
|
public string Role { get; } = Role;
|
||||||
|
public string Content { get; } = Content;
|
||||||
|
}
|
7
src/Nyanlabs.Umogen.Core/Models/IEntity.cs
Normal file
7
src/Nyanlabs.Umogen.Core/Models/IEntity.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Nyanlabs.Umogen.Core.Models;
|
||||||
|
|
||||||
|
public interface IEntity : INLSerializable
|
||||||
|
{
|
||||||
|
public string GetName();
|
||||||
|
public string? IdCode();
|
||||||
|
}
|
24
src/Nyanlabs.Umogen.Core/Models/LegalEntity.cs
Normal file
24
src/Nyanlabs.Umogen.Core/Models/LegalEntity.cs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
namespace Nyanlabs.Umogen.Core.Models;
|
||||||
|
|
||||||
|
public class LegalEntity(string Name, string? Nip = null, string? Headquarters = null, Person? Representative = null) : IEntity, INLSerializable
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = Name;
|
||||||
|
public string? Nip { get; set; } = Nip;
|
||||||
|
public string? Headquarters { get; set; } = Headquarters;
|
||||||
|
public Person? Representative { get; set; } = Representative;
|
||||||
|
|
||||||
|
public string GetName()
|
||||||
|
{
|
||||||
|
return Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? IdCode()
|
||||||
|
{
|
||||||
|
return Nip;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string NLSerialize()
|
||||||
|
{
|
||||||
|
return $"{Name}; with NIP {Nip ?? "unknown"}{(Headquarters != null ? $"; located in {Headquarters}" : "")}{(Representative != null ? $"; represented by {Representative.GetName()}" : "")}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
namespace Nyanlabs.Umogen.Core.Models;
|
namespace Nyanlabs.Umogen.Core.Models;
|
||||||
|
|
||||||
public record Person(string Name, string Surname, string? Id = null, string? Pesel = null, DateTime? DoB = null) : INLSerializable
|
public record Person(string Name, string Surname, string? Id = null, string? Pesel = null, DateTime? DoB = null) : IEntity, INLSerializable
|
||||||
{
|
{
|
||||||
public string Name { get; set; } = Name;
|
public string Name { get; set; } = Name;
|
||||||
public string Surname { get; set; } = Surname;
|
public string Surname { get; set; } = Surname;
|
||||||
|
@ -8,6 +8,16 @@ public record Person(string Name, string Surname, string? Id = null, string? Pes
|
||||||
public string? Pesel { get; set; } = Pesel;
|
public string? Pesel { get; set; } = Pesel;
|
||||||
public DateTime? DoB { get; set; } = DoB;
|
public DateTime? DoB { get; set; } = DoB;
|
||||||
|
|
||||||
|
public string GetName()
|
||||||
|
{
|
||||||
|
return $"{Name} {Surname}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? IdCode()
|
||||||
|
{
|
||||||
|
return IDCode!;
|
||||||
|
}
|
||||||
|
|
||||||
public string NLSerialize()
|
public string NLSerialize()
|
||||||
{
|
{
|
||||||
return $"{Name} {Surname}; {(Pesel != null ? $"PESEL {Pesel}" : "unknown PESEL")}; {(IDCode != null ? $"ID series {IDCode}" : "unknown ID series")}; {(DoB != null ? $"born {DoB.Value:dd MMM yyyy}" : "unknown date of birth")}";
|
return $"{Name} {Surname}; {(Pesel != null ? $"PESEL {Pesel}" : "unknown PESEL")}; {(IDCode != null ? $"ID series {IDCode}" : "unknown ID series")}; {(DoB != null ? $"born {DoB.Value:dd MMM yyyy}" : "unknown date of birth")}";
|
||||||
|
|
|
@ -30,7 +30,7 @@ public static class UmoDoctypeBindings
|
||||||
|
|
||||||
public class UmoDocument : INLSerializable
|
public class UmoDocument : INLSerializable
|
||||||
{
|
{
|
||||||
public UmoDocument(UmoDoctype type, Person employer, Person employee, ValidTime? validTime = null)
|
public UmoDocument(UmoDoctype type, IEntity employer, Person employee, ValidTime? validTime = null)
|
||||||
{
|
{
|
||||||
Doctype = type;
|
Doctype = type;
|
||||||
Employer = employer;
|
Employer = employer;
|
||||||
|
@ -39,12 +39,12 @@ public class UmoDocument : INLSerializable
|
||||||
}
|
}
|
||||||
|
|
||||||
public UmoDoctype Doctype { get; set; }
|
public UmoDoctype Doctype { get; set; }
|
||||||
public Person Employer { get; set; }
|
public IEntity Employer { get; set; }
|
||||||
public Person Employee { get; set; }
|
public Person Employee { get; set; }
|
||||||
//Null is 'unspecified time'
|
//Null is 'unspecified time'
|
||||||
public ValidTime? ValidTime { get; set; }
|
public ValidTime? ValidTime { get; set; }
|
||||||
public string NLSerialize()
|
public string NLSerialize()
|
||||||
{
|
{
|
||||||
return $"{Doctype.Name().Item1} for {Employee.NLSerialize()} working for {Employer.NLSerialize()}; valid {ValidTime?.NLSerialize() ?? "for unspecified time"}.";
|
return $"{Doctype.Name().Item2} for {Employee.NLSerialize()} working for {Employer.NLSerialize()}; valid {ValidTime?.NLSerialize() ?? "for unspecified time"}.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
13
src/Nyanlabs.Umogen.Core/Models/UsageStats.cs
Normal file
13
src/Nyanlabs.Umogen.Core/Models/UsageStats.cs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Nyanlabs.Umogen.Core.Models;
|
||||||
|
|
||||||
|
public class UsageStats
|
||||||
|
{
|
||||||
|
[JsonPropertyName("completion_tokens")]
|
||||||
|
public int CompletionTokens { get; set; }
|
||||||
|
[JsonPropertyName("prompt_tokens")]
|
||||||
|
public int PromptTokens { get; set; }
|
||||||
|
[JsonPropertyName("total_tokens")]
|
||||||
|
public int TotalTokens { get; set; }
|
||||||
|
}
|
45
src/Nyanlabs.Umogen.Core/SseConsumer.cs
Normal file
45
src/Nyanlabs.Umogen.Core/SseConsumer.cs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Nyanlabs.Umogen.Core;
|
||||||
|
|
||||||
|
public class SseConsumer<TEvent> : IDisposable, IAsyncDisposable
|
||||||
|
{
|
||||||
|
public SseConsumer(Stream stream)
|
||||||
|
{
|
||||||
|
_stream = stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Stream _stream;
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<TEvent> ReadEvents()
|
||||||
|
{
|
||||||
|
var reader = new StreamReader(_stream);
|
||||||
|
|
||||||
|
while (!reader.EndOfStream)
|
||||||
|
{
|
||||||
|
string line = (await reader.ReadLineAsync())!;
|
||||||
|
//Second \n
|
||||||
|
await reader.ReadLineAsync();
|
||||||
|
|
||||||
|
//Remove 'data: '
|
||||||
|
line = line[6..].Trim();
|
||||||
|
|
||||||
|
yield return JsonSerializer.Deserialize<TEvent>(line, Umogen.JSON_OPTS) ?? throw new DataException("Invalid data...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_stream.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
return _stream.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,7 +38,7 @@ public class UmoEngine : IDisposable
|
||||||
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly HttpClient _http;
|
internal readonly HttpClient _http;
|
||||||
|
|
||||||
public async Task<bool> ValidateKey()
|
public async Task<bool> ValidateKey()
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
using System.Data;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
using Nyanlabs.Umogen.Core.Models;
|
||||||
|
|
||||||
namespace Nyanlabs.Umogen.Core;
|
namespace Nyanlabs.Umogen.Core;
|
||||||
|
|
||||||
public class UmoProcess
|
public class UmoProcess
|
||||||
|
@ -9,8 +15,40 @@ public class UmoProcess
|
||||||
|
|
||||||
private readonly UmoEngine _eng;
|
private readonly UmoEngine _eng;
|
||||||
|
|
||||||
public async Task Ask()
|
public async IAsyncEnumerable<string> Ask(string content)
|
||||||
{
|
{
|
||||||
|
ApiRequest req = new()
|
||||||
|
{
|
||||||
|
Messages = new List<GptMessage>()
|
||||||
|
{
|
||||||
|
new("system", Umogen.PROMPT),
|
||||||
|
new("user", content)
|
||||||
|
},
|
||||||
|
Stream = true
|
||||||
|
};
|
||||||
|
|
||||||
|
HttpRequestMessage msg = new(HttpMethod.Post, "chat/completions");
|
||||||
|
msg.Content = new StringContent(JsonSerializer.Serialize(req, options: Umogen.JSON_OPTS), Encoding.UTF8, "application/json");
|
||||||
|
var resp = await _eng._http.SendAsync(msg, HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
Console.WriteLine(await resp.Content.ReadAsStringAsync());
|
||||||
|
throw new DataException($"Cannot process request: {resp.StatusCode}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiStream = await resp.Content.ReadAsStreamAsync();
|
||||||
|
|
||||||
|
using var sseConsumer = new SseConsumer<ApiResponse>(apiStream);
|
||||||
|
await foreach (var chunk in sseConsumer.ReadEvents())
|
||||||
|
{
|
||||||
|
ApiChoice choice = chunk.Choices.First();
|
||||||
|
if (choice.FinishReason == "stop")
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return choice.Delta?.Content ?? throw new DataException("Message delta is null");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Nyanlabs.Umogen.Core;
|
namespace Nyanlabs.Umogen.Core;
|
||||||
|
|
||||||
public class Umogen
|
public class Umogen
|
||||||
{
|
{
|
||||||
public const string DEFAULT_API_KEY_FILE = "umogenkey.secret";
|
public const string DEFAULT_API_KEY_FILE = "umogenkey.secret";
|
||||||
public const string PROMPT = "You write Polish legal document in markdown format.";
|
public const string PROMPT = "You write Polish legal contracts in Polish language in markdown format. Fill unknown fields with signature placeholder (underscore) with enough space to sign it with pen.";
|
||||||
|
public static readonly JsonSerializerOptions JSON_OPTS = new(JsonSerializerDefaults.Web)
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ public class NLSerializationTests
|
||||||
var query = doc.NLSerialize();
|
var query = doc.NLSerialize();
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.Equal($"Contract of Employment for {ModelEmployee.NLSerialize()} working for {ModelEmployer.NLSerialize()}; valid for unspecified time.", query);
|
Assert.Equal($"Umowa o PracÄ™ for {ModelEmployee.NLSerialize()} working for {ModelEmployer.NLSerialize()}; valid for unspecified time.", query);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
@ -31,7 +31,7 @@ public class NLSerializationTests
|
||||||
var query = doc.NLSerialize();
|
var query = doc.NLSerialize();
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.Equal($"Contract of Employment for {ModelEmployee.NLSerialize()} working for {ModelEmployer.NLSerialize()}; valid from 17 Nov 2023 until 18 Nov 2023.", query);
|
Assert.Equal($"Umowa o PracÄ™ for {ModelEmployee.NLSerialize()} working for {ModelEmployer.NLSerialize()}; valid from 17 Nov 2023 until 18 Nov 2023.", query);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
@ -64,4 +64,19 @@ public class NLSerializationTests
|
||||||
// Then
|
// Then
|
||||||
Assert.Equal("Kotek Miauczyński; unknown PESEL; unknown ID series; unknown date of birth", query);
|
Assert.Equal("Kotek Miauczyński; unknown PESEL; unknown ID series; unknown date of birth", query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LegalEntity ModelCompany => new("Nyanbyte P.S.A.", "1313131313", "Miaumiaśna 13, Miauczki", ModelEmployer);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CompanySerializationTest()
|
||||||
|
{
|
||||||
|
// Given
|
||||||
|
LegalEntity company = ModelCompany;
|
||||||
|
|
||||||
|
// When
|
||||||
|
var query = company.NLSerialize();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
Assert.Equal("Nyanbyte P.S.A.; with NIP 1313131313; located in Miaumiaśna 13, Miauczki; represented by Kotek Miauczyński", query);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
40
test/Nyanlabs.Umogen.WebTests/GenerationTests.cs
Normal file
40
test/Nyanlabs.Umogen.WebTests/GenerationTests.cs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using Nyanlabs.Umogen.Core;
|
||||||
|
using Nyanlabs.Umogen.Core.Models;
|
||||||
|
|
||||||
|
namespace Nyanlabs.Umogen.WebTests;
|
||||||
|
|
||||||
|
public class GenerationTests
|
||||||
|
{
|
||||||
|
private Person ModelEmployer => new("Kotek", "Miauczyński", "CBS4327563", "04281308999", new DateTime(2004, 08, 13));
|
||||||
|
|
||||||
|
private Person ModelEmployee => new("Miau", "Kotczyński", "CBS69696969", "76011694336", new DateTime(1976, 1, 16));
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Meow4Me()
|
||||||
|
{
|
||||||
|
// Given
|
||||||
|
var rootDir = Environment.CurrentDirectory.Replace("test/Nyanlabs.Umogen.WebTests/bin/Debug/net8.0", "");
|
||||||
|
|
||||||
|
UmoEngine eng = new(Path.Combine(rootDir, Core.Umogen.DEFAULT_API_KEY_FILE));
|
||||||
|
|
||||||
|
// When
|
||||||
|
UmoDocument doc = new(UmoDoctype.CONTR_EMPLOYMENT, ModelEmployer, ModelEmployee);
|
||||||
|
|
||||||
|
UmoProcess proc = new(eng);
|
||||||
|
var enu = proc.Ask(doc.NLSerialize());
|
||||||
|
|
||||||
|
await foreach (var str in enu)
|
||||||
|
{
|
||||||
|
Console.Write(str);
|
||||||
|
File.AppendAllText("/tmp/doc.md", str);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("~");
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue