| | | 1 | | using Microsoft.Extensions.AI; |
| | | 2 | | |
| | | 3 | | namespace NexusLabs.Needlr.AgentFramework.Iterative; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Extensions that convert <see cref="IterationRecord"/> and collections of iteration |
| | | 7 | | /// records into Microsoft.Extensions.AI shapes suitable for evaluation libraries |
| | | 8 | | /// (e.g., Microsoft.Extensions.AI.Evaluation's <c>ToolCallAccuracyEvaluator</c>). |
| | | 9 | | /// </summary> |
| | | 10 | | public static class IterationRecordEvaluationExtensions |
| | | 11 | | { |
| | | 12 | | /// <summary> |
| | | 13 | | /// Materializes a tool-call trajectory as a list of <see cref="ChatMessage"/> |
| | | 14 | | /// instances suitable for Microsoft.Extensions.AI.Evaluation trajectory-aware |
| | | 15 | | /// evaluators. Each tool call becomes an assistant message containing a |
| | | 16 | | /// <see cref="FunctionCallContent"/>, immediately followed by a tool message |
| | | 17 | | /// containing the matching <see cref="FunctionResultContent"/>. |
| | | 18 | | /// </summary> |
| | | 19 | | /// <param name="record">The iteration record to convert.</param> |
| | | 20 | | /// <returns> |
| | | 21 | | /// An ordered trajectory of <see cref="ChatMessage"/> entries. Empty if the |
| | | 22 | | /// iteration produced no tool calls. |
| | | 23 | | /// </returns> |
| | | 24 | | public static IReadOnlyList<ChatMessage> ToToolCallTrajectory(this IterationRecord record) |
| | | 25 | | { |
| | 8 | 26 | | ArgumentNullException.ThrowIfNull(record); |
| | | 27 | | |
| | 7 | 28 | | if (record.ToolCalls.Count == 0) |
| | | 29 | | { |
| | 2 | 30 | | return Array.Empty<ChatMessage>(); |
| | | 31 | | } |
| | | 32 | | |
| | 5 | 33 | | var messages = new List<ChatMessage>(record.ToolCalls.Count * 2); |
| | 26 | 34 | | for (var i = 0; i < record.ToolCalls.Count; i++) |
| | | 35 | | { |
| | 8 | 36 | | var call = record.ToolCalls[i]; |
| | 8 | 37 | | var callId = $"i{record.Iteration}-c{i}"; |
| | | 38 | | |
| | 8 | 39 | | var callContent = new FunctionCallContent( |
| | 8 | 40 | | callId: callId, |
| | 8 | 41 | | name: call.FunctionName, |
| | 8 | 42 | | arguments: call.Arguments is null |
| | 8 | 43 | | ? null |
| | 8 | 44 | | : new Dictionary<string, object?>(call.Arguments)); |
| | 8 | 45 | | messages.Add(new ChatMessage(ChatRole.Assistant, [callContent])); |
| | | 46 | | |
| | 8 | 47 | | var resultContent = new FunctionResultContent( |
| | 8 | 48 | | callId: callId, |
| | 8 | 49 | | result: call.Succeeded |
| | 8 | 50 | | ? ToolResultSerializer.Serialize(call.Result) |
| | 8 | 51 | | : call.ErrorMessage); |
| | 8 | 52 | | messages.Add(new ChatMessage(ChatRole.Tool, [resultContent])); |
| | | 53 | | } |
| | | 54 | | |
| | 5 | 55 | | return messages; |
| | | 56 | | } |
| | | 57 | | |
| | | 58 | | /// <summary> |
| | | 59 | | /// Materializes a tool-call trajectory across an entire iterative run by |
| | | 60 | | /// concatenating the trajectories of each iteration in order. |
| | | 61 | | /// </summary> |
| | | 62 | | /// <param name="records">The iteration records to flatten.</param> |
| | | 63 | | /// <returns> |
| | | 64 | | /// An ordered trajectory of <see cref="ChatMessage"/> entries across all |
| | | 65 | | /// iterations. Empty if no iteration produced any tool calls. |
| | | 66 | | /// </returns> |
| | | 67 | | public static IReadOnlyList<ChatMessage> ToToolCallTrajectory( |
| | | 68 | | this IEnumerable<IterationRecord> records) |
| | | 69 | | { |
| | 2 | 70 | | ArgumentNullException.ThrowIfNull(records); |
| | | 71 | | |
| | 1 | 72 | | var messages = new List<ChatMessage>(); |
| | 8 | 73 | | foreach (var record in records) |
| | | 74 | | { |
| | 18 | 75 | | foreach (var message in record.ToToolCallTrajectory()) |
| | | 76 | | { |
| | 6 | 77 | | messages.Add(message); |
| | | 78 | | } |
| | | 79 | | } |
| | | 80 | | |
| | 1 | 81 | | return messages; |
| | | 82 | | } |
| | | 83 | | } |