|
10 | 10 | using System.Text.Json; |
11 | 11 | using System.Text.Json.Nodes; |
12 | 12 | using System.Text.Json.Serialization; |
| 13 | +using System.Text.Json.Serialization.Metadata; |
13 | 14 | using System.Threading; |
14 | 15 | using System.Threading.Tasks; |
15 | 16 | using Microsoft.Extensions.DependencyInjection; |
|
18 | 19 | #pragma warning disable IDE0004 // Remove Unnecessary Cast |
19 | 20 | #pragma warning disable S103 // Lines should not be too long |
20 | 21 | #pragma warning disable S107 // Methods should not have too many parameters |
| 22 | +#pragma warning disable S1144 // Unused private types or members should be removed (accessed via reflection) |
21 | 23 | #pragma warning disable S2760 // Sequential tests should not check the same condition |
22 | 24 | #pragma warning disable S3358 // Ternary operators should not be nested |
23 | 25 | #pragma warning disable S5034 // "ValueTask" should be consumed correctly |
@@ -338,16 +340,154 @@ public async Task Parameters_MappedByAIParameterNameAttribute_Async() |
338 | 340 | { |
339 | 341 | AIFunction func = AIFunctionFactory.Create(([AIParameterName("$select")] string select, int top) => select + top); |
340 | 342 |
|
341 | | - AssertExtensions.EqualFunctionCallResults("Name2", await func.InvokeAsync(new() |
342 | | - { |
343 | | - ["$select"] = "Name", |
344 | | - ["top"] = 2, |
345 | | - })); |
| 343 | + AssertExtensions.EqualFunctionCallResults("Name2", await func.InvokeAsync(new() { ["$select"] = "Name", ["top"] = 2 })); |
346 | 344 |
|
347 | 345 | ArgumentException ex = await Assert.ThrowsAsync<ArgumentException>(() => func.InvokeAsync(new() { ["top"] = 2 }).AsTask()); |
348 | 346 | Assert.Contains("$select", ex.Message); |
349 | 347 | } |
350 | 348 |
|
| 349 | + [Fact] |
| 350 | + public void AIParameterNameAttribute_OverridesSchemaPropertyName() |
| 351 | + { |
| 352 | + AIFunction func = AIFunctionFactory.Create( |
| 353 | + ([AIParameterName("my_param")] string myParam, int top) => myParam + top); |
| 354 | + |
| 355 | + JsonElement expectedSchema = JsonDocument.Parse(""" |
| 356 | + { |
| 357 | + "type": "object", |
| 358 | + "properties": { |
| 359 | + "my_param": { "type": "string" }, |
| 360 | + "top": { "type": "integer" } |
| 361 | + }, |
| 362 | + "required": ["my_param", "top"] |
| 363 | + } |
| 364 | + """).RootElement; |
| 365 | + |
| 366 | + AssertExtensions.EqualJsonValues(expectedSchema, func.JsonSchema); |
| 367 | + } |
| 368 | + |
| 369 | + [Fact] |
| 370 | + public async Task AIParameterNameAttribute_BindsArgumentByOverriddenName_Async() |
| 371 | + { |
| 372 | + AIFunction func = AIFunctionFactory.Create( |
| 373 | + ([AIParameterName("$select")] string select, |
| 374 | + [AIParameterName("$expand")] string expand, |
| 375 | + string filter) => |
| 376 | + $"select='{select}', expand='{expand}', filter='{filter}'"); |
| 377 | + |
| 378 | + object? result = await func.InvokeAsync(new() |
| 379 | + { |
| 380 | + ["$select"] = "Name,Id", |
| 381 | + ["$expand"] = "Orders", |
| 382 | + ["filter"] = "Active", |
| 383 | + }); |
| 384 | + |
| 385 | + AssertExtensions.EqualFunctionCallResults("select='Name,Id', expand='Orders', filter='Active'", result); |
| 386 | + } |
| 387 | + |
| 388 | + [Fact] |
| 389 | + public async Task AIParameterNameAttribute_MissingRequiredArgument_ReportsSchemaName_Async() |
| 390 | + { |
| 391 | + AIFunction func = AIFunctionFactory.Create( |
| 392 | + ([AIParameterName("my_param")] string myParam) => myParam); |
| 393 | + |
| 394 | + ArgumentException ex = await Assert.ThrowsAsync<ArgumentException>(() => func.InvokeAsync().AsTask()); |
| 395 | + |
| 396 | + Assert.Contains("my_param", ex.Message); |
| 397 | + } |
| 398 | + |
| 399 | + [Fact] |
| 400 | + public async Task AIParameterNameAttribute_HonoredByStrictUnmappedMemberHandling_Async() |
| 401 | + { |
| 402 | + JsonSerializerOptions strictOptions = new(AIJsonUtilities.DefaultOptions) |
| 403 | + { |
| 404 | + UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, |
| 405 | + }; |
| 406 | + |
| 407 | + AIFunction func = AIFunctionFactory.Create( |
| 408 | + ([AIParameterName("my_param")] string myParam) => myParam, |
| 409 | + new AIFunctionFactoryOptions { SerializerOptions = strictOptions }); |
| 410 | + |
| 411 | + // The overridden name is "expected", so it passes strict validation. |
| 412 | + AssertExtensions.EqualFunctionCallResults("Name", await func.InvokeAsync(new() { ["my_param"] = "Name" })); |
| 413 | + |
| 414 | + // The underlying C# name is now an unexpected argument. |
| 415 | + ArgumentException ex = await Assert.ThrowsAsync<ArgumentException>("arguments", async () => |
| 416 | + await func.InvokeAsync(new() { ["myParam"] = "Name" })); |
| 417 | + Assert.Contains("myParam", ex.Message); |
| 418 | + } |
| 419 | + |
| 420 | + [Fact] |
| 421 | + public async Task AIParameterNameAttribute_InheritedByOverride_Async() |
| 422 | + { |
| 423 | + MethodInfo overrideMethod = typeof(MyDerivedType).GetMethod(nameof(MyDerivedType.Method))!; |
| 424 | + AIFunction func = AIFunctionFactory.Create(overrideMethod, new MyDerivedType()); |
| 425 | + |
| 426 | + Assert.Contains("my_param", func.JsonSchema.ToString()); |
| 427 | + Assert.DoesNotContain("\"myParam\"", func.JsonSchema.ToString()); |
| 428 | + |
| 429 | + AssertExtensions.EqualFunctionCallResults("param='Name'", await func.InvokeAsync(new() { ["my_param"] = "Name" })); |
| 430 | + } |
| 431 | + |
| 432 | + [Fact] |
| 433 | + public async Task AIFunctionAndParameterNameAttributes_BothHonored_Async() |
| 434 | + { |
| 435 | + AIFunction func = AIFunctionFactory.Create([AIFunctionName("my_tool")] ([AIParameterName("my_param")] string myParam) => myParam); |
| 436 | + |
| 437 | + Assert.Equal("my_tool", func.Name); |
| 438 | + Assert.Contains("my_param", func.JsonSchema.ToString()); |
| 439 | + |
| 440 | + AssertExtensions.EqualFunctionCallResults("Name", await func.InvokeAsync(new() { ["my_param"] = "Name" })); |
| 441 | + } |
| 442 | + |
| 443 | + [Fact] |
| 444 | + public void AIFunctionAndParameterNameAttributes_NameAndParameterSchema_PreservedByAsDeclarationOnly() |
| 445 | + { |
| 446 | + AIFunction func = AIFunctionFactory.Create([AIFunctionName("my_tool")] ([AIParameterName("my_param")] string myParam) => myParam); |
| 447 | + |
| 448 | + AIFunctionDeclaration declaration = func.AsDeclarationOnly(); |
| 449 | + |
| 450 | + Assert.Equal("my_tool", declaration.Name); |
| 451 | + Assert.Equal(func.JsonSchema.ToString(), declaration.JsonSchema.ToString()); |
| 452 | + Assert.Contains("my_param", declaration.JsonSchema.ToString()); |
| 453 | + Assert.IsNotAssignableFrom<AIFunction>(declaration); |
| 454 | + } |
| 455 | + |
| 456 | + [Fact] |
| 457 | + public void AIParameterNameAttribute_EscapesNameInJsonPointerRef() |
| 458 | + { |
| 459 | + JsonSerializerOptions options = new(AIJsonUtilities.DefaultOptions) { TypeInfoResolver = new DefaultJsonTypeInfoResolver() }; |
| 460 | + |
| 461 | + AIFunction func = AIFunctionFactory.Create( |
| 462 | + ([AIParameterName("a/b~c")] AIParameterNameAttributeRecursiveNode node) => node.ToString(), |
| 463 | + new AIFunctionFactoryOptions { SerializerOptions = options }); |
| 464 | + |
| 465 | + string schema = func.JsonSchema.ToString(); |
| 466 | + |
| 467 | + Assert.Contains("#/properties/a~1b~0c", schema); |
| 468 | + Assert.DoesNotContain("#/properties/a/b~c", schema); |
| 469 | + } |
| 470 | + |
| 471 | + [Fact] |
| 472 | + public void AIParameterNameAttribute_DuplicateNames_Throw() |
| 473 | + { |
| 474 | + ArgumentException ex = Assert.Throws<ArgumentException>(() => AIFunctionFactory.Create( |
| 475 | + ([AIParameterName("dup")] string first, [AIParameterName("dup")] string second) => first + second)); |
| 476 | + Assert.Contains("dup", ex.Message); |
| 477 | + |
| 478 | + ArgumentException ex2 = Assert.Throws<ArgumentException>(() => AIFunctionFactory.Create( |
| 479 | + ([AIParameterName("filter")] string select, string filter) => select + filter)); |
| 480 | + Assert.Contains("filter", ex2.Message); |
| 481 | + } |
| 482 | + |
| 483 | + [Fact] |
| 484 | + public void AIFunctionNameAttribute_DisplayNameAttribute_StillHonoredWhenNoAIFunctionNameAttribute() |
| 485 | + { |
| 486 | + AIFunction func = AIFunctionFactory.Create([DisplayName("from-display-name")] () => "result"); |
| 487 | + |
| 488 | + Assert.Equal("from-display-name", func.Name); |
| 489 | + } |
| 490 | + |
351 | 491 | [Fact] |
352 | 492 | public void AIFunctionFactoryCreateOptions_ValuesPropagateToAIFunction() |
353 | 493 | { |
@@ -1625,4 +1765,19 @@ public async Task Parameters_UnmappedMemberHandlingDisallow_CustomBindParameter_ |
1625 | 1765 | [JsonSerializable(typeof(int?))] |
1626 | 1766 | [JsonSerializable(typeof(DateTime?))] |
1627 | 1767 | private partial class JsonContext : JsonSerializerContext; |
| 1768 | + |
| 1769 | + private abstract class MyBaseType |
| 1770 | + { |
| 1771 | + public abstract string Method([AIParameterName("my_param")] string myParam); |
| 1772 | + } |
| 1773 | + |
| 1774 | + private sealed class MyDerivedType : MyBaseType |
| 1775 | + { |
| 1776 | + public override string Method(string myParam) => $"param='{myParam}'"; |
| 1777 | + } |
| 1778 | + |
| 1779 | + private sealed class AIParameterNameAttributeRecursiveNode |
| 1780 | + { |
| 1781 | + public AIParameterNameAttributeRecursiveNode? Next { get; set; } |
| 1782 | + } |
1628 | 1783 | } |
0 commit comments