Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@iceljc
Copy link
Collaborator

@iceljc iceljc commented Dec 18, 2025

PR Type

Enhancement


Description

  • Add ThoughtSignature property to conversation models for storing AI reasoning

  • Propagate thought signature through dialog metadata and storage layers

  • Integrate thought signature handling in Google AI chat completion provider

  • Update MongoDB storage mapping to persist thought signature data


Diagram Walkthrough

flowchart LR
  A["DialogMetaData"] -->|"adds property"| B["ThoughtSignature"]
  B -->|"propagated through"| C["RoleDialogModel"]
  C -->|"stored in"| D["ConversationStorage"]
  D -->|"persisted via"| E["MongoStorage"]
  F["GoogleAI Provider"] -->|"extracts from"| G["API Response"]
  G -->|"creates"| H["ChatThoughtModel"]
  H -->|"assigns to"| C
Loading

File Walkthrough

Relevant files
Enhancement
8 files
Conversation.cs
Add ThoughtSignature to DialogMetaData model                         
+4/-0     
RoleDialogModel.cs
Add ThoughtSignature property and update From method         
+5/-1     
ConversationStorage.cs
Map ThoughtSignature in dialog retrieval operations           
+3/-0     
RoutingService.InvokeAgent.cs
Assign ThoughtSignature from response to message                 
+2/-0     
ChatThoughtModel.cs
Create new model for storing thought and tool call data   
+9/-0     
ChatCompletionProvider.cs
Extract and propagate thought signature in chat completions
+40/-17 
RealTimeCompletionProvider.cs
Add ThoughtSignature to realtime content parts                     
+2/-0     
DialogMongoElement.cs
Add ThoughtSignature to MongoDB dialog metadata mapping   
+3/-0     
Configuration changes
1 files
Using.cs
Add global using for ChatThoughtModel namespace                   
+1/-0     
Formatting
1 files
ChatCompletionProvider.cs
Refactor streaming message creation for consistency           
+5/-6     

@qodo-code-review
Copy link

qodo-code-review bot commented Dec 18, 2025

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Sensitive metadata persistence

Description: The PR adds end-to-end propagation and persistence of ThoughtSignature (e.g.,
ThoughtSignature = meta.ThoughtSignature / ThoughtSignature = meta.ThoughtSignature) which
can embed sensitive model “reasoning”, hidden prompt-derived metadata, or other
confidential signals and may be inadvertently exposed via logs/exports/backups or
downstream APIs that serialize dialog metadata.
DialogMongoElement.cs [43-90]

Referred Code
public class DialogMetaDataMongoElement
{
    public string Role { get; set; } = default!;
    public string AgentId { get; set; } = default!;
    public string MessageId { get; set; } = default!;
    public string MessageType { get; set; } = default!;
    public string? MessageLabel { get; set; }
    public string? FunctionName { get; set; }
    public string? FunctionArgs { get; set; }
    public string? ToolCallId { get; set; }
    public string? ThoughtSignature { get; set; }
    public string? SenderId { get; set; }
    public DateTime CreateTime { get; set; }

    public static DialogMetaData ToDomainElement(DialogMetaDataMongoElement meta)
    {
        return new DialogMetaData
        {
            Role = meta.Role,
            AgentId = meta.AgentId,
            MessageId = meta.MessageId,


 ... (clipped 27 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
New field persisted: The PR introduces persistence of ThoughtSignature without any accompanying audit logging
to confirm whether this newly stored data should be treated as a critical/sensitive write
requiring audit trail events.

Referred Code
public string? ThoughtSignature { get; set; }
public string? SenderId { get; set; }
public DateTime CreateTime { get; set; }

public static DialogMetaData ToDomainElement(DialogMetaDataMongoElement meta)
{
    return new DialogMetaData
    {
        Role = meta.Role,
        AgentId = meta.AgentId,
        MessageId = meta.MessageId,
        MessageType = meta.MessageType,
        MessageLabel = meta.MessageLabel,
        FunctionName = meta.FunctionName,
        FunctionArgs = meta.FunctionArgs,
        ToolCallId = meta.ToolCallId,
        ThoughtSignature = meta.ThoughtSignature,
        SenderId = meta.SenderId,
        CreatedTime = meta.CreateTime,
    };
}


 ... (clipped 17 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Unbounded external data: ThoughtSignature is accepted from an external provider response and propagated/stored
without visible validation (e.g., null/empty normalization, max length, allowed
characters), which may cause unexpected storage or downstream processing issues depending
on system constraints not shown in the diff.

Referred Code
        ToolCallId = toolCall?.Id,
        FunctionName = toolCall?.Name,
        FunctionArgs = toolCall?.Args?.ToJsonString(),
        ThoughtSignature = part?.ThoughtSignature,
        RenderedInstruction = string.Join("\r\n", renderedInstructions)
    };
}
else
{
    responseMessage = new RoleDialogModel(AgentRole.Assistant, text)
    {
        CurrentAgentId = agent.Id,
        MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty,
        ThoughtSignature = part?.ThoughtSignature,
        RenderedInstruction = string.Join("\r\n", renderedInstructions)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Sensitive data retention: The PR adds ThoughtSignature to conversation metadata and persists/propagates it
end-to-end, but the diff does not show any classification, consent/retention controls, or
sanitization to ensure this potentially sensitive AI-generated reasoning artifact is
handled according to security and privacy requirements.

Referred Code
[JsonPropertyName("thought_signature")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ThoughtSignature { get; set; }

[JsonPropertyName("sender_id")]
public string? SenderId { get; set; }

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link

qodo-code-review bot commented Dec 18, 2025

PR Code Suggestions ✨

Latest suggestions up to 0373266

CategorySuggestion                                                                                                                                    Impact
Possible issue
Prevent JSON parse crashes

Add defensive parsing for message.FunctionArgs to prevent crashes from invalid
JSON. Use a helper method to try parsing and fall back to an empty JSON object
on failure.

src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs [421-426]

 FunctionCall = new FunctionCall
 {
     Id = message.ToolCallId,
     Name = message.FunctionName,
-    Args = JsonNode.Parse(message.FunctionArgs ?? "{}")
+    Args = TryParseJsonNode(message.FunctionArgs) ?? new JsonObject()
 }
 
+...
+
+static JsonNode? TryParseJsonNode(string? json)
+{
+    if (string.IsNullOrWhiteSpace(json)) return null;
+    try { return JsonNode.Parse(json); }
+    catch { return null; }
+}
+

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a potential JsonException if message.FunctionArgs is not a valid JSON string, which would crash the request building process. Implementing the proposed defensive parsing makes the code more robust.

Medium
Ensure tool call id exists

Ensure ToolCallId is never null or empty by providing a fallback. If the ID is
missing, use the tool name or a newly generated GUID as a substitute.

src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs [61-73]

 responseMessage = new RoleDialogModel(AgentRole.Function, text)
 {
     CurrentAgentId = agent.Id,
     MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty,
-    ToolCallId = toolCall?.Id,
+    ToolCallId = !string.IsNullOrWhiteSpace(toolCall?.Id)
+        ? toolCall!.Id
+        : (!string.IsNullOrWhiteSpace(toolCall?.Name) ? toolCall!.Name : Guid.NewGuid().ToString("N")),
     FunctionName = toolCall?.Name,
     FunctionArgs = toolCall?.Args?.ToJsonString(),
     MetaData = new Dictionary<string, string?>
     {
         [Constants.ThoughtSignature] = part?.ThoughtSignature
     },
     RenderedInstruction = string.Join("\r\n", renderedInstructions)
 };
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out a potential issue where a missing ToolCallId could break downstream logic. Providing a fallback mechanism improves the application's robustness, although the likelihood of the ID being missing is not guaranteed.

Medium
Incremental [*]
Prevent shared mutable metadata
Suggestion Impact:Updated message.MetaData assignment to create a new dictionary from response.MetaData (or null), preventing shared mutable metadata between objects.

code diff:

             message = RoleDialogModel.From(message, role: AgentRole.Assistant, content: response.Content);
             message.CurrentAgentId = agent.Id;
-            message.MetaData = response.MetaData;
+            message.MetaData = response.MetaData != null ? new(response.MetaData) : null;
             message.IsStreaming = response.IsStreaming;

To prevent shared state bugs from mutable dictionaries, create a new MetaData
dictionary instance for the message instead of assigning it by reference from
response.MetaData.

src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs [76-79]

 message = RoleDialogModel.From(message, role: AgentRole.Assistant, content: response.Content);
 message.CurrentAgentId = agent.Id;
-message.MetaData = response.MetaData;
+message.MetaData = response.MetaData != null ? new(response.MetaData) : null;
 message.IsStreaming = response.IsStreaming;

[Suggestion processed]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a potential shared state bug by assigning a dictionary by reference and proposes creating a new instance, which is consistent with another branch in the same method and prevents subtle bugs.

Medium
Align metadata JSON field name

Add the [JsonPropertyName("meta_data")] attribute to the MetaData property in
RoleDialogModel to ensure consistent snake_case serialization, matching other
properties and related models.

src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs [74-75]

 [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+[JsonPropertyName("meta_data")]
 public Dictionary<string, string?>? MetaData { get; set; }
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a JSON serialization inconsistency for the new MetaData property and proposes a fix that aligns it with related models like DialogMetaData, improving API consistency.

Low
Avoid emitting null metadata entries

Conditionally create the MetaData dictionary only when part.ThoughtSignature has
a value to avoid creating dictionaries with a single null entry and to produce
cleaner JSON.

src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs [68-71]

-MetaData = new Dictionary<string, string?>
-{
-    [Constants.ThoughtSignature] = part?.ThoughtSignature
-},
+MetaData = string.IsNullOrWhiteSpace(part?.ThoughtSignature)
+    ? null
+    : new Dictionary<string, string?>
+    {
+        [Constants.ThoughtSignature] = part!.ThoughtSignature
+    },
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly points out that an empty MetaData dictionary is created when ThoughtSignature is null. The fix avoids creating an empty dictionary, leading to cleaner serialized output, which is a good practice.

Low
  • Update

Previous suggestions

Suggestions up to commit 0373266
CategorySuggestion                                                                                                                                    Impact
Incremental [*]
Preserve and conditionally store signatures

Conditionally create the MetaData dictionary only when a thoughtSignature
exists, and use the cached thoughtModel to ensure the signature is not lost in
streaming responses.

src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs [68-71]

-MetaData = new Dictionary<string, string?>
+var thoughtSignature = part?.ThoughtSignature;
+MetaData = string.IsNullOrWhiteSpace(thoughtSignature) ? null : new Dictionary<string, string?>
 {
-    [Constants.ThoughtSignature] = part?.ThoughtSignature
+    [Constants.ThoughtSignature] = thoughtSignature
 },
 ...
+var finalThoughtSignature = thought?.ThoughtSignature;
 responseMessage = new RoleDialogModel(AgentRole.Assistant, allText)
 {
     CurrentAgentId = agent.Id,
     MessageId = messageId,
     IsStreaming = true,
-    MetaData = new Dictionary<string, string?>
+    MetaData = string.IsNullOrWhiteSpace(finalThoughtSignature) ? null : new Dictionary<string, string?>
     {
-        [Constants.ThoughtSignature] = part?.ThoughtSignature
+        [Constants.ThoughtSignature] = finalThoughtSignature
     }
 };
 ...
+var finishThoughtSignature = thoughtModel?.ThoughtSignature ?? part?.ThoughtSignature;
 responseMessage = new RoleDialogModel(AgentRole.Assistant, text)
 {
     CurrentAgentId = agent.Id,
     MessageId = messageId,
     IsStreaming = true,
-    MetaData = new Dictionary<string, string?>
+    MetaData = string.IsNullOrWhiteSpace(finishThoughtSignature) ? null : new Dictionary<string, string?>
     {
-        [Constants.ThoughtSignature] = part?.ThoughtSignature
+        [Constants.ThoughtSignature] = finishThoughtSignature
     }
 };
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out two issues: it avoids serializing empty metadata dictionaries and prevents losing the thoughtSignature in streaming mode by using a cached value. This improves data integrity and reduces unnecessary data storage.

Medium
Deep-copy metadata dictionaries

Create a new copy of the MetaData dictionary when mapping between domain and
storage models to prevent shared-reference bugs.

src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs [79]

 ToolCallId = meta?.ToolCallId,
 FunctionName = meta?.FunctionName,
 FunctionArgs = meta?.FunctionArgs,
-MetaData = meta?.MetaData,
+MetaData = meta?.MetaData != null ? new(meta.MetaData) : null,
 RichContent = richContent,
 ...
 ToolCallId = dialog.ToolCallId,
 FunctionName = dialog.FunctionName,
 FunctionArgs = dialog.FunctionArgs,
-MetaData = dialog.MetaData,
+MetaData = dialog.MetaData != null ? new(dialog.MetaData) : null,
 CreatedTime = dialog.CreatedAt
 ...
 FunctionName = dialog.FunctionName,
-MetaData = dialog.MetaData,
+MetaData = dialog.MetaData != null ? new(dialog.MetaData) : null,
 CreatedTime = dialog.CreatedAt
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that MetaData dictionaries are being assigned by reference, which can lead to unintended side effects. Creating a new copy ensures object encapsulation and prevents hard-to-debug mutation bugs, aligning with best practices already used elsewhere in the PR.

Medium
Align metadata JSON field name

Add the [JsonPropertyName("meta_data")] attribute to the MetaData property in
RoleDialogModel to ensure consistent JSON serialization.

src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs [74-75]

 [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+[JsonPropertyName("meta_data")]
 public Dictionary<string, string?>? MetaData { get; set; }
Suggestion importance[1-10]: 6

__

Why: This suggestion correctly identifies a potential JSON serialization inconsistency between RoleDialogModel.MetaData and DialogMetaData.MetaData, which could lead to client-side or data persistence issues. Applying it improves API consistency and robustness.

Low
✅ Suggestions up to commit 5938742
CategorySuggestion                                                                                                                                    Impact
Possible issue
Preserve streaming thought signature

In GetChatCompletionsStreamingAsync, ensure ThoughtSignature is preserved by
using the cached thoughtModel as a fallback when constructing the final
responseMessage, as the final streaming chunk may not contain it.

src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs [253-299]

 if (candidate!.FinishReason == FinishReason.STOP)
 {
     var thought = part?.FunctionCall != null
         ? new() { ToolCall = part.FunctionCall, ThoughtSignature = part.ThoughtSignature }
         : thoughtModel;
     var functionCall = thought?.ToolCall;
 
     if (functionCall != null)
     {
         responseMessage = new RoleDialogModel(AgentRole.Function, string.Empty)
         {
             CurrentAgentId = agent.Id,
             MessageId = messageId,
             ToolCallId = functionCall.Id,
             FunctionName = functionCall.Name,
             FunctionArgs = functionCall.Args?.ToJsonString(),
             FunctionMetaData = new Dictionary<string, string?>
             {
-                [Constants.ThoughtSignature] = thought?.ThoughtSignature
+                [Constants.ThoughtSignature] = thought?.ThoughtSignature ?? part?.ThoughtSignature
             }
         };
         ...
     }
     else
     {
         var allText = textStream.GetText();
         ...
 
         responseMessage = new RoleDialogModel(AgentRole.Assistant, allText)
         {
             CurrentAgentId = agent.Id,
             MessageId = messageId,
             IsStreaming = true,
             FunctionMetaData = new Dictionary<string, string?>
             {
-                [Constants.ThoughtSignature] = part?.ThoughtSignature
+                [Constants.ThoughtSignature] = part?.ThoughtSignature ?? thoughtModel?.ThoughtSignature
             }
         };
     }
 
     tokenUsage = response?.UsageMetadata;
 }
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a potential data loss bug in streaming mode where ThoughtSignature from an earlier chunk could be lost, and the proposed fix correctly uses the cached value to prevent this.

Medium
Guard against invalid tool calls

In GetChatCompletions, add a null or whitespace check for toolCall.Name after
calling response.GetFunction() to prevent creating an invalid function call
message and fall back to an assistant message if the name is missing.

src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs [58-74]

-if (response.GetFunction() != null)
+var toolCall = response.GetFunction();
+if (toolCall != null && !string.IsNullOrWhiteSpace(toolCall.Name))
 {
-    var toolCall = response.GetFunction();
     responseMessage = new RoleDialogModel(AgentRole.Function, text)
     {
         CurrentAgentId = agent.Id,
         MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty,
-        ToolCallId = toolCall?.Id,
-        FunctionName = toolCall?.Name,
-        FunctionArgs = toolCall?.Args?.ToJsonString(),
+        ToolCallId = toolCall.Id,
+        FunctionName = toolCall.Name,
+        FunctionArgs = toolCall.Args?.ToJsonString(),
+        FunctionMetaData = new Dictionary<string, string?>
+        {
+            [Constants.ThoughtSignature] = part?.ThoughtSignature
+        },
+        RenderedInstruction = string.Join("\r\n", renderedInstructions)
+    };
+}
+else
+{
+    responseMessage = new RoleDialogModel(AgentRole.Assistant, text)
+    {
+        CurrentAgentId = agent.Id,
+        MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty,
         FunctionMetaData = new Dictionary<string, string?>
         {
             [Constants.ThoughtSignature] = part?.ThoughtSignature
         },
         RenderedInstruction = string.Join("\r\n", renderedInstructions)
     };
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that response.GetFunction() can be non-null while its properties like Name are null, leading to an invalid function call message. Adding a check for toolCall.Name improves the robustness of the logic.

Medium
Align metadata JSON field name
Suggestion Impact:Instead of adding a JsonPropertyName attribute to FunctionMetaData, the commit renamed the property from FunctionMetaData to MetaData and updated the cloning/mapping logic accordingly. This changes the serialized JSON field name via the property rename, addressing metadata naming inconsistency in a different way than suggested.

code diff:

     [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
-    public Dictionary<string, string?>? FunctionMetaData { get; set; }
+    public Dictionary<string, string?>? MetaData { get; set; }
 
     /// <summary>
     /// Set this flag is in OnFunctionExecuting, if true, it won't be executed by InvokeFunction.
@@ -195,7 +195,6 @@
             ToolCallId = source.ToolCallId,
             FunctionName = source.FunctionName,
             FunctionArgs = source.FunctionArgs,
-            FunctionMetaData = source.FunctionMetaData != null ? new(source.FunctionMetaData) : null,
             Indication = source.Indication,
             PostbackFunctionName = source.PostbackFunctionName,
             RichContent = source.RichContent,
@@ -204,7 +203,8 @@
             Instruction = source.Instruction,
             Data = source.Data,
             IsStreaming = source.IsStreaming,
-            Annotations = source.Annotations
+            Annotations = source.Annotations,
+            MetaData = source.MetaData != null ? new(source.MetaData) : null
         };

Add the [JsonPropertyName("function_data")] attribute to the FunctionMetaData
property in RoleDialogModel to ensure consistent JSON serialization with
DialogMetaData.

src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs [74-75]

 [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+[JsonPropertyName("function_data")]
 public Dictionary<string, string?>? FunctionMetaData { get; set; }
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies an inconsistency in JSON property naming between RoleDialogModel.FunctionMetaData and DialogMetaData.FunctionMetaData, which could lead to serialization issues and improves code robustness.

Low
Incremental [*]
Filter empty metadata before storing

Filter out null or empty entries from FunctionMetaData before assignment, and
set the dictionary to null if it becomes empty to avoid persisting meaningless
data.

src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs [59]

-message.FunctionMetaData = response.FunctionMetaData != null ? new(response.FunctionMetaData) : null;
+message.FunctionMetaData = response.FunctionMetaData?
+    .Where(kv => !string.IsNullOrWhiteSpace(kv.Key) && !string.IsNullOrWhiteSpace(kv.Value))
+    .ToDictionary(kv => kv.Key, kv => kv.Value);
 
+if (message.FunctionMetaData?.Count == 0)
+{
+    message.FunctionMetaData = null;
+}
+
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies that FunctionMetaData can be populated with null values, and proposes filtering them to improve data cleanliness. This is a good practice for maintainability, though not a critical issue.

Low
✅ Suggestions up to commit 8e90ae2
CategorySuggestion                                                                                                                                    Impact
Incremental [*]
Persist function arguments in storage
Suggestion Impact:The commit adds back the missing assignment `FunctionArgs = dialog.FunctionArgs` when building the stored dialog element, preventing loss of function call arguments.

code diff:

@@ -113,6 +113,7 @@
                 MessageLabel = dialog.MessageLabel,
                 ToolCallId = dialog.ToolCallId,
                 FunctionName = dialog.FunctionName,
+                FunctionArgs = dialog.FunctionArgs,
                 FunctionMetaData = dialog.FunctionMetaData,
                 CreatedTime = dialog.CreatedAt

Restore the assignment of FunctionArgs in BuildDialogElement to prevent losing
function call arguments when persisting conversation history.

src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs [114-117]

 ToolCallId = dialog.ToolCallId,
 FunctionName = dialog.FunctionName,
+FunctionArgs = dialog.FunctionArgs,
 FunctionMetaData = dialog.FunctionMetaData,
 CreatedTime = dialog.CreatedAt

[Suggestion processed]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies that FunctionArgs was accidentally removed when saving conversation dialogs, which would lead to data loss and break functionality.

High
Avoid shared metadata reference
Suggestion Impact:Updated the assignment of message.FunctionMetaData to create a new dictionary from response.FunctionMetaData when non-null, preventing shared state issues.

code diff:

             message.ToolCallId = response.ToolCallId;
             message.FunctionName = response.FunctionName;
             message.FunctionArgs = response.FunctionArgs;
-            message.FunctionMetaData = response.FunctionMetaData;
+            message.FunctionMetaData = response.FunctionMetaData != null ? new(response.FunctionMetaData) : null;

Create a new dictionary instance for message.FunctionMetaData instead of
assigning by reference to prevent potential shared state issues.

src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs [59]

-message.FunctionMetaData = response.FunctionMetaData;
+message.FunctionMetaData = response.FunctionMetaData != null ? new(response.FunctionMetaData) : null;

[Suggestion processed]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly points out that assigning a dictionary by reference can lead to unintended side effects, and proposes a defensive copy, which is consistent with patterns elsewhere in the PR.

Low
Store metadata only when present

Conditionally create FunctionMetaData only when part.ThoughtSignature has a
value to avoid unnecessary allocations and empty metadata entries.

src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs [68-71]

-FunctionMetaData = new Dictionary<string, string?>
-{
-    [Constants.ThoughtSignature] = part?.ThoughtSignature
-},
+FunctionMetaData = !string.IsNullOrWhiteSpace(part?.ThoughtSignature)
+    ? new Dictionary<string, string?> { [Constants.ThoughtSignature] = part!.ThoughtSignature }
+    : null,
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly points out that FunctionMetaData is allocated even when its value is null, and proposes to conditionally allocate it, which improves memory usage and data cleanliness.

Low
Possible issue
Prevent JSON parse crashes

Add a try-catch block around JsonNode.Parse for FunctionArgs to handle potential
malformed JSON and prevent the application from crashing.

src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs [425]

-Args = JsonNode.Parse(message.FunctionArgs ?? "{}")
+Args = TryParseArgs(message.FunctionArgs)
Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies a potential runtime exception from parsing invalid JSON in FunctionArgs and proposes a robust solution to prevent crashes, which significantly improves error handling.

Medium
Copy metadata to avoid aliasing
Suggestion Impact:The commit removed the direct assignment of FunctionMetaData in the mappings (replacing it with MetaData instead), which eliminates the specific aliasing risk the suggestion called out for FunctionMetaData. However, it did not implement the suggested defensive copy (`new(...)`) for FunctionMetaData.

code diff:

@@ -76,7 +76,7 @@
                 ToolCallId = meta?.ToolCallId,
                 FunctionName = meta?.FunctionName,
                 FunctionArgs = meta?.FunctionArgs,
-                FunctionMetaData = meta?.FunctionMetaData,
+                MetaData = meta?.MetaData,
                 RichContent = richContent,
                 SecondaryContent = secondaryContent,
                 SecondaryRichContent = secondaryRichContent,
@@ -113,7 +113,8 @@
                 MessageLabel = dialog.MessageLabel,
                 ToolCallId = dialog.ToolCallId,
                 FunctionName = dialog.FunctionName,
-                FunctionMetaData = dialog.FunctionMetaData,
+                FunctionArgs = dialog.FunctionArgs,
+                MetaData = dialog.MetaData,
                 CreatedTime = dialog.CreatedAt
             };
 
@@ -140,7 +141,7 @@
                 MessageLabel = dialog.MessageLabel,
                 SenderId = dialog.SenderId,
                 FunctionName = dialog.FunctionName,
-                FunctionMetaData = dialog.FunctionMetaData,
+                MetaData = dialog.MetaData,
                 CreatedTime = dialog.CreatedAt

Create a new dictionary for FunctionMetaData when mapping from storage to
RoleDialogModel to prevent shared mutable state issues.

src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs [79]

-FunctionMetaData = meta?.FunctionMetaData,
+FunctionMetaData = meta?.FunctionMetaData != null ? new(meta.FunctionMetaData) : null,
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out a potential issue with mutable state by assigning a dictionary by reference. Creating a new dictionary instance is a good defensive programming practice.

Medium
✅ Suggestions up to commit e185f43
CategorySuggestion                                                                                                                                    Impact
High-level
Feature implementation is provider-specific
Suggestion Impact:Instead of adding ThoughtSignature support to other providers (e.g., OpenAI), the commit refactored the core model to remove the dedicated ThoughtSignature property and replaced it with a generic FunctionMetaData dictionary. GoogleAI now stores ThoughtSignature under a metadata key, and when sending messages back to GoogleAI it reads the value from FunctionMetaData. This reduces provider-specific coupling in the core model, directly addressing the inconsistency highlighted in the suggestion via a different approach.

code diff:

# File: src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs
@@ -63,16 +63,16 @@
     public string? FunctionName { get; set; }
 
     [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+    public string? PostbackFunctionName { get; set; }
+
+    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+    public string? FunctionArgs { get; set; }
+
+    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
     public string? ToolCallId { get; set; }
 
     [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
-    public string? PostbackFunctionName { get; set; }
-
-    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
-    public string? FunctionArgs { get; set; }
-
-    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
-    public string? ThoughtSignature { get; set; }
+    public Dictionary<string, string?>? FunctionMetaData { get; set; }
 
     /// <summary>
     /// Set this flag is in OnFunctionExecuting, if true, it won't be executed by InvokeFunction.
@@ -192,9 +192,10 @@
             MessageId = source.MessageId,
             MessageType = source.MessageType,
             MessageLabel = source.MessageLabel,
+            ToolCallId = source.ToolCallId,
+            FunctionName = source.FunctionName,
             FunctionArgs = source.FunctionArgs,
-            FunctionName = source.FunctionName,
-            ToolCallId = source.ToolCallId,
+            FunctionMetaData = source.FunctionMetaData != null ? new(source.FunctionMetaData) : null,
             Indication = source.Indication,
             PostbackFunctionName = source.PostbackFunctionName,
             RichContent = source.RichContent,
@@ -203,8 +204,7 @@
             Instruction = source.Instruction,
             Data = source.Data,
             IsStreaming = source.IsStreaming,
-            Annotations = source.Annotations,
-            ThoughtSignature = source.ThoughtSignature
+            Annotations = source.Annotations
         };
     }
 }

# File: src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs
@@ -65,7 +65,10 @@
                 ToolCallId = toolCall?.Id,
                 FunctionName = toolCall?.Name,
                 FunctionArgs = toolCall?.Args?.ToJsonString(),
-                ThoughtSignature = part?.ThoughtSignature,
+                FunctionMetaData = new Dictionary<string, string?>
+                {
+                    [Constants.ThoughtSignature] = part?.ThoughtSignature
+                },
                 RenderedInstruction = string.Join("\r\n", renderedInstructions)
             };
         }
@@ -75,7 +78,10 @@
             {
                 CurrentAgentId = agent.Id,
                 MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty,
-                ThoughtSignature = part?.ThoughtSignature,
+                FunctionMetaData = new Dictionary<string, string?>
+                {
+                    [Constants.ThoughtSignature] = part?.ThoughtSignature
+                },
                 RenderedInstruction = string.Join("\r\n", renderedInstructions)
             };
         }
@@ -119,7 +125,10 @@
         var msg = new RoleDialogModel(AgentRole.Assistant, text)
         {
             CurrentAgentId = agent.Id,
-            ThoughtSignature = part?.ThoughtSignature,
+            FunctionMetaData = new Dictionary<string, string?>
+            {
+                [Constants.ThoughtSignature] = part?.ThoughtSignature
+            },
             RenderedInstruction = string.Join("\r\n", renderedInstructions)
         };
 
@@ -148,7 +157,10 @@
                 ToolCallId = toolCall?.Id,
                 FunctionName = toolCall?.Name,
                 FunctionArgs = toolCall?.Args?.ToJsonString(),
-                ThoughtSignature = part?.ThoughtSignature,
+                FunctionMetaData = new Dictionary<string, string?>
+                {
+                    [Constants.ThoughtSignature] = part?.ThoughtSignature
+                },
                 RenderedInstruction = string.Join("\r\n", renderedInstructions)
             };
 
@@ -254,7 +266,10 @@
                         ToolCallId = functionCall.Id,
                         FunctionName = functionCall.Name,
                         FunctionArgs = functionCall.Args?.ToJsonString(),
-                        ThoughtSignature = thought?.ThoughtSignature
+                        FunctionMetaData = new Dictionary<string, string?>
+                        {
+                            [Constants.ThoughtSignature] = thought?.ThoughtSignature
+                        }
                     };
 
 #if DEBUG
@@ -272,8 +287,11 @@
                     {
                         CurrentAgentId = agent.Id,
                         MessageId = messageId,
-                        ThoughtSignature = part?.ThoughtSignature,
-                        IsStreaming = true
+                        IsStreaming = true,
+                        FunctionMetaData = new Dictionary<string, string?>
+                        {
+                            [Constants.ThoughtSignature] = part?.ThoughtSignature
+                        }
                     };
                 }
 
@@ -286,8 +304,11 @@
                 {
                     CurrentAgentId = agent.Id,
                     MessageId = messageId,
-                    ThoughtSignature = part?.ThoughtSignature,
-                    IsStreaming = true
+                    IsStreaming = true,
+                    FunctionMetaData = new Dictionary<string, string?>
+                    {
+                        [Constants.ThoughtSignature] = part?.ThoughtSignature
+                    }
                 };
 
                 tokenUsage = response?.UsageMetadata;
@@ -396,7 +417,7 @@
                 contents.Add(new Content([
                     new Part()
                     {
-                        ThoughtSignature = message.ThoughtSignature,
+                        ThoughtSignature = message.FunctionMetaData?.GetValueOrDefault(Constants.ThoughtSignature, null),
                         FunctionCall = new FunctionCall
                         {
                             Id = message.ToolCallId,
@@ -409,7 +430,7 @@
                 contents.Add(new Content([
                     new Part()
                     {
-                        ThoughtSignature = message.ThoughtSignature,
+                        ThoughtSignature = message.FunctionMetaData?.GetValueOrDefault(Constants.ThoughtSignature, null),
                         FunctionResponse = new FunctionResponse
                         {
                             Id = message.ToolCallId,
@@ -429,7 +450,11 @@
                 var text = message.LlmContent;
                 var contentParts = new List<Part>
                 {
-                    new() { Text = text, ThoughtSignature = message.ThoughtSignature }
+                    new()
+                    {
+                        Text = text,
+                        ThoughtSignature = message.FunctionMetaData?.GetValueOrDefault(Constants.ThoughtSignature, null)
+                    }
                 };
 
                 if (allowMultiModal && !message.Files.IsNullOrEmpty())
@@ -444,7 +469,11 @@
                 var text = message.LlmContent;
                 var contentParts = new List<Part>
                 {
-                    new() { Text = text, ThoughtSignature = message.ThoughtSignature }
+                    new()
+                    {
+                        Text = text,
+                        ThoughtSignature = message.FunctionMetaData?.GetValueOrDefault(Constants.ThoughtSignature, null)
+                    }
                 };

The ThoughtSignature property has been added to core models but is only
implemented for the Google AI provider. This creates an architectural
inconsistency, as other providers like OpenAI do not support this feature.

Examples:

src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs [74-75]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string? ThoughtSignature { get; set; }
src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs [68]
                ThoughtSignature = part?.ThoughtSignature,

Solution Walkthrough:

Before:

// In BotSharp.Abstraction/.../RoleDialogModel.cs
public class RoleDialogModel
{
    // ... other properties
    public string? ThoughtSignature { get; set; }
}

// In GoogleAI/.../ChatCompletionProvider.cs
public async Task<RoleDialogModel> GetChatCompletions(...)
{
    var response = await aiModel.GenerateContentAsync(request);
    var part = response.Candidates?.First()?.Content?.Parts?.FirstOrDefault();
    var responseMessage = new RoleDialogModel(...) {
        // ...
        ThoughtSignature = part?.ThoughtSignature
    };
    return responseMessage;
}

// In OpenAI/.../ChatCompletionProvider.cs
// No implementation to populate `ThoughtSignature`.

After:

// In BotSharp.Abstraction/.../RoleDialogModel.cs
public class RoleDialogModel
{
    // ... other properties
    public string? ThoughtSignature { get; set; }
}

// In GoogleAI/.../ChatCompletionProvider.cs
public async Task<RoleDialogModel> GetChatCompletions(...)
{
    // ... (implementation remains)
    var responseMessage = new RoleDialogModel(...) {
        ThoughtSignature = ... 
    };
    return responseMessage;
}

// In OpenAI/.../ChatCompletionProvider.cs
// A corresponding implementation should be added for OpenAI and other providers.
public async Task<RoleDialogModel> GetChatCompletions(...)
{
    // Hypothetical logic to extract a similar concept from OpenAI response
    var responseMessage = new RoleDialogModel(...) {
        ThoughtSignature = ExtractThoughtFromOpenAiResponse(response)
    };
    return responseMessage;
}
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a significant architectural inconsistency where ThoughtSignature is added to core models but only implemented in the Google AI provider, undermining the platform's provider-agnostic design.

High
General
Add JSON name mapping

Add the [JsonPropertyName("thought_signature")] attribute to the
ThoughtSignature property to ensure consistent snake_case JSON naming.

src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs [74-75]

+[JsonPropertyName("thought_signature")]
 [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
 public string? ThoughtSignature { get; set; }
Suggestion importance[1-10]: 7

__

Why: This suggestion correctly identifies a missing JsonPropertyName attribute, which improves consistency with the project's snake_case naming convention for JSON serialization.

Medium
Learned
best practice
Remove null-forgiving and guard nulls

Avoid using the null-forgiving operator on candidate and guard part access
explicitly so streaming completion doesn't throw on unexpected/null frames.

src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs [241-281]

-if (candidate!.FinishReason == FinishReason.STOP)
+if (candidate?.FinishReason == FinishReason.STOP)
 {
     var thought = part?.FunctionCall != null
         ? new() { ToolCall = part.FunctionCall, ThoughtSignature = part.ThoughtSignature }
         : thoughtModel;
     var functionCall = thought?.ToolCall;
 
     if (functionCall != null)
     {
         responseMessage = new RoleDialogModel(AgentRole.Function, string.Empty)
         {
             CurrentAgentId = agent.Id,
             MessageId = messageId,
             ToolCallId = functionCall.Id,
             FunctionName = functionCall.Name,
             FunctionArgs = functionCall.Args?.ToJsonString(),
             ThoughtSignature = thought?.ThoughtSignature
         };
         ...
     }
     else
     {
         ...
         responseMessage = new RoleDialogModel(AgentRole.Assistant, allText)
         {
             CurrentAgentId = agent.Id,
             MessageId = messageId,
             ThoughtSignature = part?.ThoughtSignature,
             IsStreaming = true
         };
     }
 
     tokenUsage = response?.UsageMetadata;
 }
Suggestion importance[1-10]: 6

__

Why:
Relevant best practice - Improve defensive coding with precise null and state checks to prevent crashes and incorrect behavior.

Low

@iceljc iceljc merged commit 57097b5 into SciSharp:master Dec 18, 2025
0 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant