Compare commits

..

5 commits

Author SHA1 Message Date
3135428fb2
Merge branch 'nya' into HEAD 2023-11-18 01:06:54 +01:00
4c1da1fa7d
EPIC LICENCING 🥺🥺🥺🥺🥺 2023-11-18 01:02:08 +01:00
067a5365d2
Refactor Server Sent Events parsing 2023-11-17 23:38:35 +01:00
b9c218111a
Add legal entity 2023-11-17 23:37:35 +01:00
7b322ef7fb
absolute utter filth yet it's working 2023-11-17 22:46:30 +01:00
17 changed files with 268 additions and 9 deletions

24
LICENSE Normal file
View 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.

View 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; }
}

View file

@ -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();
} }

View 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; }
}

View file

@ -0,0 +1,6 @@
namespace Nyanlabs.Umogen.Core.Models;
public class ApiResponseFormat
{
public string Type { get; set; } = "json_object";
}

View 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;
}

View file

@ -0,0 +1,7 @@
namespace Nyanlabs.Umogen.Core.Models;
public interface IEntity : INLSerializable
{
public string GetName();
public string? IdCode();
}

View 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()}" : "")}";
}
}

View file

@ -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")}";

View file

@ -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"}.";
} }
} }

View 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; }
}

View 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();
}
}

View file

@ -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()
{ {

View file

@ -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");
}
} }
} }

View file

@ -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
};
} }

View file

@ -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);
}
} }

View 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("~");
}
}