Skip to content

Commit 3521a3b

Browse files
authored
Merge pull request #134 from cs-util-com/feature/jsonSchemaSpec2020
Feature/json schema spec2020
2 parents a37e40b + 3a18347 commit 3521a3b

File tree

13 files changed

+304
-60
lines changed

13 files changed

+304
-60
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: .NET Tests (Safe Caching + Reporting + Exclude IntegrationTests)
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
permissions:
8+
contents: read # always minimal
9+
checks: write # needed to post check run
10+
pull-requests: none
11+
12+
jobs:
13+
build-test:
14+
runs-on: ubuntu-latest
15+
permissions:
16+
contents: read
17+
checks: write
18+
pull-requests: none
19+
steps:
20+
- name: Checkout code
21+
uses: actions/checkout@v4
22+
23+
- name: Setup .NET
24+
uses: actions/setup-dotnet@v3
25+
with:
26+
dotnet-version: '8.0.x'
27+
cache: true
28+
cache-dependency-path: |
29+
**/*.csproj
30+
Directory.Packages.props
31+
32+
- name: Restore dependencies
33+
run: dotnet restore CsCore/CsCore.sln
34+
35+
- name: Build solution
36+
run: dotnet build CsCore/CsCore.sln --no-restore
37+
38+
- name: Run xUnit tests (exclude integrationTests)
39+
run: >
40+
dotnet test CsCore/xUnitTests/xUnitTests.csproj
41+
--no-build
42+
--verbosity normal
43+
--logger "trx;LogFileName=test-results.trx"
44+
--filter "FullyQualifiedName\!~integrationTests"
45+
46+
report:
47+
needs: build-test
48+
if: github.event_name != 'pull_request'
49+
|| github.event.pull_request.head.repo.full_name == github.repository
50+
runs-on: ubuntu-latest
51+
permissions:
52+
contents: read
53+
checks: write
54+
pull-requests: write
55+
steps:
56+
- name: Checkout code
57+
uses: actions/checkout@v4
58+
59+
- name: Upload and annotate test results
60+
uses: dorny/test-reporter@v1
61+
with:
62+
name: Test Results
63+
path: '**/*.trx'
64+
reporter: dotnet-trx
65+
fail-on-error: 'true'
66+

CsCore/CsCoreUnity/Plugins/CsCoreUnity/com/csutil/ui/jsonschema/JsonModelExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ public static async Task LoadModelList(this ListFieldView self, JObject root, Js
210210

211211
private static void SetupButtons(ListFieldView listView, JObject root, JsonSchemaToView viewGenerator, JArray modelArray, Dictionary<FieldView, JToken> map) {
212212
listView.add.SetOnClickAction(async delegate {
213-
JToken entry = listView.field.items.First().NewDefaultJInstance();
213+
JToken entry = listView.field.items.anyOf.First().NewDefaultJInstance();
214214
modelArray.Add(entry);
215215
var fieldName = "" + (modelArray.Count - 1);
216216
var fv = await CreateChildEntryView(listView, root, viewGenerator, entry, fieldName);
@@ -256,7 +256,7 @@ private static IEnumerable<ListEntryView> GetSelectedViews(ListFieldView self) {
256256

257257
private static async Task<FieldView> CreateChildEntryView(
258258
ListFieldView self, JObject root, JsonSchemaToView viewGenerator, JToken modelEntry, string fieldName) {
259-
JsonSchema newEntryVm = GetMatchingSchema(modelEntry, self.field.items);
259+
JsonSchema newEntryVm = GetMatchingSchema(modelEntry, self.field.items.anyOf);
260260
GameObject childView = await AddChildEntryView(self, viewGenerator, fieldName, newEntryVm);
261261
await childView.LinkToJsonModel(root, viewGenerator);
262262
return childView.GetComponentInChildren<FieldView>();

CsCore/PlainNetClassLib/src/Plugins/CsCore/com/csutil/extensions/EnumExtensions.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,21 @@ private static void AssertEnumEntriesMustBePowerOfTwo<T>() where T : struct {
6868
int entryAsInt = (int)(object)entry;
6969
if (entryAsInt == 0) { continue; } // 0 is always a power of two
7070
if ((entryAsInt & (entryAsInt - 1)) != 0) { // Check if the entry is a power of two:
71-
Log.w($"Enum {typeof(T)} cant be used with .ContainsFlag() because not all entries are a power of two, e.g. {entry}={entryAsInt}! "
72-
+ $"This error should only be ignored if you have enum entries that are composed from other enum entries (eg MyEnum.A = MyEnum.B | MyEnum.C)");
71+
var enumType = typeof(T);
72+
if (!IsOnBlacklist(enumType)) {
73+
Log.w($"Enum {enumType} cant be used with .ContainsFlag() because not all entries are a power of two, e.g. {entry}={entryAsInt}! "
74+
+ $"This error should only be ignored if you have enum entries that are composed from other enum entries (eg MyEnum.A = MyEnum.B | MyEnum.C)");
75+
}
7376
}
7477
}
7578
}
79+
80+
private static bool IsOnBlacklist(Type enumType) {
81+
// Anything in the System namespace is on the blacklist, as the dev has no control over these enums anyways:
82+
if (enumType.Namespace.StartsWith("System")) { return true; }
83+
// Add more types to the blacklist here if needed
84+
return false;
85+
}
7686

7787
public static string GetEntryName<T>(this T entry) where T : struct
7888
#if CSHARP_7_3 // Using Enum as a generic type constraint is only available in C# 7.3+

CsCore/PlainNetClassLib/src/Plugins/CsCore/com/csutil/http/apis/OpenAi.cs

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
using Newtonsoft.Json;
88
using System.IO;
99
using System.Linq;
10+
using System.Text.RegularExpressions;
11+
using com.csutil.model;
1012
using Zio;
1113

1214
namespace com.csutil.http.apis {
@@ -210,11 +212,13 @@ public class Request {
210212
/// <summary> https://platform.openai.com/docs/api-reference/chat/create#chat-create-temperature </summary>
211213
public double? temperature { get; set; }
212214

213-
/// <summary> The maximum number of tokens to generate in the completion.
214-
/// The token count of your prompt plus max_tokens cannot exceed the model's context length.
215-
/// Most models have a context length of 2048 tokens (except for the newest models, which support 4096). </summary>
216-
public int max_tokens { get; set; }
217-
215+
/// <summary>
216+
/// An upper bound for the number of tokens that can be generated for a completion,
217+
/// including visible output tokens and reasoning tokens. See also
218+
/// https://platform.openai.com/docs/api-reference/chat/create#chat-create-max_completion_tokens
219+
/// </summary>
220+
public int? max_completion_tokens { get; set; } = null;
221+
218222
/// <summary> Number of desired <see cref="Response.choices"/> to be returned, defaults to 1.
219223
/// See also https://platform.openai.com/docs/api-reference/chat/create#chat-create-n </summary>
220224
public int? n { get; set; } = null;
@@ -230,13 +234,8 @@ public class Request {
230234
/// <summary> typically null, but if the AI e.g. should respond only with json it should be ChatGpt.Request.ResponseFormat.json </summary>
231235
public ResponseFormat response_format { get; set; }
232236

233-
public Request(List<Message> messages, int max_tokens = 4096) {
234-
var tokenCountForMessages = JsonWriter.GetWriter(this).Write(messages).Length;
235-
if (max_tokens + tokenCountForMessages > 4096) {
236-
max_tokens = 4096 - tokenCountForMessages;
237-
}
237+
public Request(List<Message> messages) {
238238
this.messages = messages;
239-
this.max_tokens = max_tokens;
240239
}
241240

242241
/// <summary> https://platform.openai.com/docs/guides/audio/quickstart?audio-generation-quickstart-example=audio-out </summary>
@@ -257,10 +256,15 @@ public class ResponseFormat {
257256
public static ResponseFormat json => new ResponseFormat() { type = "json_object" };
258257

259258
/// <summary> See https://platform.openai.com/docs/guides/structured-outputs/how-to-use </summary>
260-
public static ResponseFormat NewJsonSchema(string name, JsonSchema schema) => new ResponseFormat() {
261-
type = "json_schema",
262-
json_schema = new JsonSchemaResponse(name, schema, strict: true)
263-
};
259+
public static ResponseFormat NewJsonSchema(string name, JsonSchema schema) {
260+
if (schema.additionalProperties) {
261+
throw new ArgumentException($"Invalid schema: {name} has 'additionalProperties' set to true, but this is not supported in strict json schema mode.");
262+
}
263+
return new ResponseFormat() {
264+
type = "json_schema",
265+
json_schema = new JsonSchemaResponse(name, schema, strict: true)
266+
};
267+
}
264268

265269
public string type { get; set; }
266270

@@ -282,6 +286,12 @@ public JsonSchemaResponse(string name, JsonSchema schema, bool strict) {
282286
this.name = name;
283287
this.schema = schema;
284288
this.strict = strict;
289+
if (!Regex.IsMatch(name, "^[a-zA-Z0-9_-]+$")) { // name must match the pattern '^[a-zA-Z0-9_-]+$' so verify that here:
290+
throw new ArgumentException($"Invalid 'response_format.json_schema.name': {name} does not match pattern. Expected a string that matches the pattern '^[a-zA-Z0-9_-]+$'.");
291+
}
292+
if (schema.type != "object") {
293+
throw new ArgumentException($"Invalid schema.type: {schema.type} is not supported, expected 'object'.");
294+
}
285295
}
286296

287297
}
@@ -359,20 +369,18 @@ public class Request {
359369
/// <summary> See https://beta.openai.com/docs/models/overview </summary>
360370
public string model = "gpt-4o";
361371

362-
/// <summary> The maximum number of tokens to generate in the completion.
363-
/// The token count of your prompt plus max_tokens cannot exceed the model's context length.
364-
/// Most models have a context length of 2048 tokens (except for the newest models, which support 4096). </summary>
365-
public int max_tokens { get; set; }
372+
/// <summary>
373+
/// An upper bound for the number of tokens that can be generated for a completion,
374+
/// including visible output tokens and reasoning tokens. See also
375+
/// https://platform.openai.com/docs/api-reference/chat/create#chat-create-max_completion_tokens
376+
/// </summary>
377+
public int? max_completion_tokens { get; set; } = null;
366378
public List<Line> messages { get; set; }
367379

368-
public Request(List<Line> messages, int max_tokens = 4096) {
369-
var tokenCountForMessages = JsonWriter.GetWriter(this).Write(messages).Length;
370-
if (max_tokens + tokenCountForMessages > 4096) {
371-
max_tokens = 4096 - tokenCountForMessages;
372-
}
380+
public Request(List<Line> messages) {
373381
this.messages = messages;
374-
this.max_tokens = max_tokens;
375382
}
383+
376384
}
377385

378386
public class Response {
@@ -440,6 +448,14 @@ public static void SetResponseFormatToJsonSchema<T>(this ChatGpt.Request self, T
440448
self.response_format = ChatGpt.Request.ResponseFormat.NewJsonSchema(schemaName, jsonSchema);
441449
}
442450

451+
public static void SetResponseFormatToJsonSchema<T>(this ChatGpt.Request self) {
452+
ModelToJsonSchema schemaGenerator = new ModelToJsonSchema(nullValueHandling: NullValueHandling.Ignore);
453+
Type type = typeof(T);
454+
string className = type.Name;
455+
JsonSchema jsonSchema = schemaGenerator.ToJsonSchema(className, type);
456+
self.response_format = ChatGpt.Request.ResponseFormat.NewJsonSchema(className, jsonSchema);
457+
}
458+
443459
public static JsonSchema CreateJsonSchema<T>(T exampleResponse) {
444460
var schemaGenerator = new ModelToJsonSchema(nullValueHandling: NullValueHandling.Ignore);
445461
var className = typeof(T).Name;

CsCore/PlainNetClassLib/src/Plugins/CsCore/com/csutil/model/RegexTemplates.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ public static class RegexTemplates {
4646

4747
public static class RegexUtil {
4848

49-
private static Regex camelCaseSplitter = new Regex(@"(\B[A-Z]+?(?=[A-Z][^A-Z])|\B[A-Z]+?(?=[^A-Z]))");
49+
private static Regex camelCaseSplitter = new Regex(@"(?<!^)(?=[A-Z][a-z])|(?<=[a-z])(?=[A-Z])|(?<=[A-Za-z])(?=[0-9])|(?<=[0-9])(?=[A-Za-z])", RegexOptions.Compiled);
5050

5151
public static string SplitCamelCaseString(string camelCaseString) {
52-
return camelCaseSplitter.Replace(camelCaseString, " $1").ToFirstCharUpperCase();
52+
return camelCaseSplitter.Replace(camelCaseString, " $0").ToFirstCharUpperCase();
5353
}
5454

5555
/// <summary> Combines multiple regex via AND </summary>

CsCore/PlainNetClassLib/src/Plugins/CsCore/com/csutil/model/ecs/ExpensiveRegularAsyncTaskInBackground.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ public Task SetTaskFor(string contextId, Action taskToDoInBackground) {
3636
return Task.CompletedTask;
3737
}
3838
}
39+
40+
public async Task WaitTillDone(int millisecondsDelay = 10) {
41+
while (!runningTasks.IsEmpty || !waitingTasks.IsEmpty) {
42+
var tasksToWaitFor = runningTasks.Values;
43+
if (tasksToWaitFor.IsEmpty()) {
44+
// If there are no running tasks but still waiting tasks,
45+
// a short delay is needed to allow the system to schedule the next task.
46+
await TaskV2.Delay(millisecondsDelay);
47+
} else {
48+
await Task.WhenAll(tasksToWaitFor);
49+
}
50+
}
51+
}
52+
3953
}
4054

4155
}

CsCore/PlainNetClassLib/src/Plugins/CsCore/com/csutil/model/ecs/TemplatesIO.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ public Task SaveChanges(T instance) {
9292
});
9393
}
9494

95+
public Task WaitForAllSavedChanges(int millisecondsDelay = 10) { return _taskQueue.WaitTillDone(millisecondsDelay); }
96+
9597
// [Conditional("DEBUG")]
9698
// private void LogSaveChangesIfThereIsAQueue(T instance) {
9799
// if (_taskQueue.GetRemainingScheduledTaskCount() % 50 == 0) {

CsCore/PlainNetClassLib/src/Plugins/CsCore/com/csutil/model/jsonschema/BaseJsonSchemaToView.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ public async Task<V> AddViewForJsonSchemaField(V parentView, JsonSchema field, s
6363
}
6464
if (type == JTokenType.Array) {
6565
var e = field.items;
66-
if (e.Count == 1) {
67-
JsonSchema item = e.First();
66+
if (e.anyOf.Count == 1) {
67+
JsonSchema item = e.anyOf.First();
6868
var childJType = item.GetJTokenType();
6969
if (schemaGenerator.IsSimpleType(childJType)) {
7070
return await AddAndInit(parentView, field, fieldName, await NewListFieldView(field));

CsCore/PlainNetClassLib/src/Plugins/CsCore/com/csutil/model/jsonschema/JsonSchema.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using Newtonsoft.Json;
2-
using System;
32
using System.Collections.Generic;
3+
using System.Linq;
44

55
namespace com.csutil.model.jsonschema {
66

@@ -64,7 +64,7 @@ public class JsonSchema {
6464

6565
/// <summary> If the field is an object it has a view model itself, see also
6666
/// https://json-schema.org/understanding-json-schema/reference/array.html#items </summary>
67-
public List<JsonSchema> items;
67+
public Items items;
6868

6969
/// <summary> If true items is a set so it can only contain unique items, see also
7070
/// https://json-schema.org/understanding-json-schema/reference/array.html#uniqueness </summary>
@@ -79,10 +79,23 @@ public class JsonSchema {
7979
/// https://json-schema.org/understanding-json-schema/reference/generic.html#enumerated-values </summary>
8080
[JsonProperty("enum")]
8181
public string[] contentEnum;
82-
82+
8383
public bool additionalProperties;
8484

85-
public static string ToTitle(string varName) { return RegexUtil.SplitCamelCaseString(varName); }
85+
public static string ToTitle(string varName) {
86+
var words = RegexUtil.SplitCamelCaseString(varName)
87+
.Split(' ').SelectMany(x => x.Split('_'))
88+
.Where(x => !x.IsNullOrEmpty())
89+
.Select(x => char.ToUpper(x[0]) + x.Substring(1));
90+
return string.Join(" ", words);
91+
}
92+
93+
}
94+
95+
public class Items {
96+
97+
/// <summary> The instance is valid if it matches at least one of the listed schemas. Multiple matches are fine </summary>
98+
public List<JsonSchema> anyOf;
8699

87100
}
88101

0 commit comments

Comments
 (0)