Skip to content

Conversation

@iceljc
Copy link
Collaborator

@iceljc iceljc commented Aug 13, 2025

PR Type

Enhancement


Description

  • Add agent function visibility mode with auto/manual filtering

  • Refactor vector storage to use typed payload values

  • Enhance vector filtering with complex query operators

  • Update all LLM providers to use new instruction/function preparation


Diagram Walkthrough

flowchart LR
  A["Agent Model"] --> B["Function Visibility Mode"]
  B --> C["Auto Function Filtering"]
  B --> D["Manual Function Selection"]
  E["Vector Storage"] --> F["Typed Payload Values"]
  F --> G["Enhanced Query Filters"]
  H["LLM Providers"] --> I["Unified Instruction Preparation"]
  C --> I
  D --> I
Loading

File Walkthrough

Relevant files
Enhancement
19 files
AgentField.cs
Add FuncVisMode field to agent enum                                           
+1/-0     
AgentFuncVisMode.cs
Define function visibility mode constants                               
+7/-0     
IAgentService.cs
Add function filtering and instruction preparation methods
+7/-2     
Agent.cs
Add FuncVisMode property and setter method                             
+13/-0   
VectorPayloadDataType.cs
Define vector payload data type enumeration                           
+11/-0   
VectorPayloadValue.cs
Create typed vector payload value model                                   
+58/-0   
VectorFilterGroup.cs
Enhance vector filtering with complex operators                   
+59/-3   
VectorCollectionCreateOptions.cs
Create vector collection creation options model                   
+8/-0     
IVectorDb.cs
Update interface to use typed payloads                                     
+2/-2     
AgentService.Rendering.cs
Implement function filtering and instruction preparation 
+34/-3   
ChatCompletionProvider.cs
Use new instruction and function preparation                         
+5/-8     
ChatCompletionProvider.cs
Use new instruction and function preparation                         
+8/-8     
ChatCompletionProvider.cs
Use new instruction and function preparation                         
+8/-8     
GeminiChatCompletionProvider.cs
Use new instruction and function preparation                         
+6/-6     
ChatCompletionProvider.cs
Use new instruction and function preparation                         
+9/-8     
QdrantDb.cs
Implement typed payloads and enhanced filtering                   
+303/-107
KnowledgeService.Vector.cs
Update to use typed vector payload values                               
+15/-13 
FileRepository.Agent.cs
Add FuncVisMode field update support                                         
+15/-0   
MongoRepository.Agent.cs
Add FuncVisMode field update support                                         
+16/-0   
Additional files
38 files
ICrontabHook.cs +1/-3     
IKnowledgeService.cs +2/-3     
VectorCollectionData.cs +1/-1     
VectorCollectionDetails.cs +20/-0   
VectorCreateModel.cs +1/-2     
CrontabService.cs +1/-1     
CrontabWatcher.cs +5/-1     
AgentService.UpdateAgent.cs +2/-0     
FileInstructService.cs +1/-1     
ExecuteTemplateFn.cs +1/-1     
InstructService.Execute.cs +2/-2     
KnowledgeBaseController.cs +7/-3     
AgentCreationModel.cs +8/-1     
AgentUpdateModel.cs +7/-0     
AgentViewModel.cs +6/-0     
VectorKnowledgeCreateRequest.cs +2/-4     
VectorCollectionDetailsViewModel.cs +2/-1     
VectorKnowledgeViewModel.cs +1/-1     
PalmChatCompletionProvider.cs +6/-8     
RealTimeCompletionProvider.cs +6/-6     
ChatCompletionProvider.cs +1/-1     
MemorizeKnowledgeFn.cs +2/-2     
MemoryVectorDb.cs +8/-4     
KnowledgeService.Document.cs +15/-15 
KnowledgeService.Index.cs +4/-2     
ChatCompletionProvider.cs +1/-1     
ChatCompletionProvider.cs +3/-3     
MicrosoftExtensionsAIChatCompletionProvider.cs +19/-18 
AgentDocument.cs +1/-0     
RealTimeCompletionProvider.cs +7/-7     
Using.cs +2/-0     
SemanticKernelChatCompletionProvider.cs +3/-3     
SemanticKernelMemoryStoreProvider.cs +7/-3     
ChatCompletionProvider.cs +6/-4     
BotSharp.Plugin.SqlDriver.csproj +3/-4     
DbKnowledgeService.cs +2/-2     
TestAgentService.cs +17/-2   
SemanticKernelChatCompletionProviderTests.cs +1/-1     

@qodo-merge-pro
Copy link

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Possible Issue

The auto function filtering uses a simple regex to collect words from the instruction and matches them directly to function names. This can over-filter (missing valid functions due to naming conventions, hyphens, casing, plurals) or include unintended tokens. Verify the matching logic and consider safer heuristics or opt-in keywords.

public IEnumerable<FunctionDef> FilterFunctions(string instruction, Agent agent, StringComparer? comparer = null)
{
    var functions = agent.Functions.AsEnumerable();

    if (agent.FuncVisMode.IsEqualTo(AgentFuncVisMode.Auto) && !string.IsNullOrWhiteSpace(instruction))
    {
        comparer = comparer ?? StringComparer.OrdinalIgnoreCase;
        var matches = Regex.Matches(instruction, @"\b[A-Za-z0-9_]+\b");
        var words = new HashSet<string>(matches.Select(m => m.Value), comparer);
        functions = functions.Where(x => words.Contains(x.Name, comparer));
    }

    functions = functions.Concat(agent.SecondaryFunctions ?? []);
    return functions;
}
API Consistency

The JSON field name for function visibility mode is snake_case ('function_visibility_mode') in API models, while the domain model uses camelCase ('functionVisibilityMode'). Confirm this intentional divergence and ensure documentation/clients are updated.

/// <summary>
/// Agent function visibility mode
/// </summary>
[JsonPropertyName("function_visibility_mode")]
public string? FuncVisMode { get; set; }
Debug Behavior

Conditional compilation runs cron handlers directly in DEBUG but publishes events otherwise. Ensure side-effects are acceptable and that behavior differences won’t mask issues in production paths.

#if DEBUG
                    await HandleCrontabEvent(item);
#else
                    if (publisher != null)
                    {
                        await publisher.PublishAsync($"Crontab:{item.Title}", item.Cron);
                    }
                    else
                    {
                        await HandleCrontabEvent(item);
                    }
#endif

@qodo-merge-pro
Copy link

qodo-merge-pro bot commented Aug 13, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Risky function auto-filtering

The new Auto function visibility filters tools by matching bare words in the
rendered instruction, which is fragile and can hide needed functions across all
providers. Consider centralizing a robust, opt-in filtering strategy (e.g.,
explicit tags/allowlists or LLM-guided selection with fallback) and ensure
providers always include secondary/critical functions to avoid unintended
capability loss.

Examples:

src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs [110-124]
public IEnumerable<FunctionDef> FilterFunctions(string instruction, Agent agent, StringComparer? comparer = null)
{
    var functions = agent.Functions.AsEnumerable();

    if (agent.FuncVisMode.IsEqualTo(AgentFuncVisMode.Auto) && !string.IsNullOrWhiteSpace(instruction))
    {
        comparer = comparer ?? StringComparer.OrdinalIgnoreCase;
        var matches = Regex.Matches(instruction, @"\b[A-Za-z0-9_]+\b");
        var words = new HashSet<string>(matches.Select(m => m.Value), comparer);
        functions = functions.Where(x => words.Contains(x.Name, comparer));

 ... (clipped 5 lines)

Solution Walkthrough:

Before:

public IEnumerable<FunctionDef> FilterFunctions(string instruction, Agent agent)
{
    var functions = agent.Functions;
    if (agent.FuncVisMode == "Auto" && !string.IsNullOrEmpty(instruction))
    {
        var words = new HashSet<string>(Regex.Matches(instruction, @"\b[A-Za-z0-9_]+\b").Select(m => m.Value));
        functions = functions.Where(x => words.Contains(x.Name));
    }
    return functions.Concat(agent.SecondaryFunctions ?? []);
}

After:

public IEnumerable<FunctionDef> FilterFunctions(string instruction, Agent agent)
{
    var functions = agent.Functions;
    if (agent.FuncVisMode == "Auto" && !string.IsNullOrEmpty(instruction))
    {
        // A more robust strategy, e.g., using explicit markers in the instruction
        // or an LLM call to determine relevant functions.
        functions = SelectFunctionsBasedOnInstructionSemantics(instruction, functions);
    }
    return functions.Concat(agent.SecondaryFunctions ?? []);
}
Suggestion importance[1-10]: 9

__

Why: This suggestion correctly identifies a significant design flaw in the new FilterFunctions logic, where the fragile word-matching approach can unintentionally disable agent capabilities across all providers.

High
Possible issue
Fix comparer misuse and null checks
Suggestion Impact:The commit refactored function filtering but still misused HashSet.Contains with a comparer in the new helper; however, it adopted null-safe handling elsewhere and partially addressed comparer defaulting. Overall, it shows partial uptake but not a full correct fix.

code diff:

+        comparer = comparer ?? StringComparer.OrdinalIgnoreCase;
+        var matches = Regex.Matches(instruction, @"\b[A-Za-z0-9_-]+\b");
+        var words = new HashSet<string>(matches.Select(m => m.Value), comparer);
+        return functions.Where(x => words.Contains(x.Name, comparer));

Avoid passing a comparer to HashSet.Contains — it only accepts the element.
Also, null-check function names to prevent NREs. Build the set with the comparer
and call Contains with a single argument, and ensure x.Name is not null before
accessing.

src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs [110-124]

 public IEnumerable<FunctionDef> FilterFunctions(string instruction, Agent agent, StringComparer? comparer = null)
 {
     var functions = agent.Functions.AsEnumerable();
 
     if (agent.FuncVisMode.IsEqualTo(AgentFuncVisMode.Auto) && !string.IsNullOrWhiteSpace(instruction))
     {
-        comparer = comparer ?? StringComparer.OrdinalIgnoreCase;
+        comparer ??= StringComparer.OrdinalIgnoreCase;
         var matches = Regex.Matches(instruction, @"\b[A-Za-z0-9_]+\b");
         var words = new HashSet<string>(matches.Select(m => m.Value), comparer);
-        functions = functions.Where(x => words.Contains(x.Name, comparer));
+        functions = functions.Where(x => !string.IsNullOrEmpty(x.Name) && words.Contains(x.Name));
     }
 
     functions = functions.Concat(agent.SecondaryFunctions ?? []);
     return functions;
 }

[Suggestion processed]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies that calling HashSet.Contains with a comparer is a compilation error and provides a valid fix, which is critical for the code's correctness.

High
General
Skip functions with invalid names

Guard against null or empty function names before adding tools to avoid runtime
exceptions in tool creation. Skip functions without a valid Name.

src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs [578-589]

 var functions = agentService.FilterFunctions(instruction, agent);
 foreach (var function in functions)
 {
+    if (string.IsNullOrWhiteSpace(function.Name)) continue;
     if (!agentService.RenderFunction(agent, function)) continue;
 
     var property = agentService.RenderFunctionProperty(agent, function);
 
     options.Tools.Add(ChatTool.CreateFunctionTool(
         functionName: function.Name,
         functionDescription: function.Description,
         functionParameters: BinaryData.FromObjectAsJson(property)));
 }
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion adds a sensible guard clause to check for null or whitespace function.Name, improving the code's robustness against potential runtime errors.

Low
  • Update

@iceljc iceljc marked this pull request as draft August 15, 2025 03:18
@iceljc iceljc marked this pull request as ready for review August 18, 2025 22:26
@qodo-merge-pro
Copy link

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Possible Issue

In BuildMatchCondition, "neq" handling routes to MustNot but BuildMatchFieldCondition only supports boolean/integer/text; other types (double, datetime) are not supported. Additionally, BuildRangeFieldCondition for datetime computes nanos via ticks % second * 100 which may be incorrect for protobuf Timestamp nanoseconds. Validate correctness and type coverage to avoid incorrect filters.

private Condition? BuildMatchCondition(VectorFilterMatch match)
{
    var fieldCondition = BuildMatchFieldCondition(match);
    if (fieldCondition == null)
    {
        return null;
    }

    Condition condition;
    if (match.Operator.IsEqualTo("eq"))
    {
        var filter = new Filter()
        {
            Must = { new Condition { Field = fieldCondition } }
        };
        condition = new Condition { Filter = filter };
    }
    else
    {
        var filter = new Filter()
        {
            MustNot = { new Condition { Field = fieldCondition } }
        };
        condition = new Condition { Filter = filter };
    }

    return condition;
}

private FieldCondition? BuildMatchFieldCondition(VectorFilterMatch match)
{
    if (string.IsNullOrEmpty(match.Key) || match.Value == null)
    {
        return null;
    }

    var fieldCondition = new FieldCondition { Key = match.Key };

    if (match.DataType == VectorPayloadDataType.Boolean
        && bool.TryParse(match.Value, out var boolVal))
    {
        fieldCondition.Match = new Match { Boolean = boolVal };
    }
    else if (match.DataType == VectorPayloadDataType.Integer
        && long.TryParse(match.Value, out var longVal))
    {
        fieldCondition.Match = new Match { Integer = longVal };
    }
    else
    {
        fieldCondition.Match = new Match { Text = match.Value };
    }

    return fieldCondition;
}

private Condition? BuildRangeCondition(VectorFilterRange range)
{
    var fieldCondition = BuildRangeFieldCondition(range);
    if (fieldCondition == null)
    {
        return null;
    }

    return new Condition
    {
        Field = fieldCondition
    };
}

private FieldCondition? BuildRangeFieldCondition(VectorFilterRange range)
{
    if (string.IsNullOrEmpty(range.Key) || range.Conditions.IsNullOrEmpty())
    {
        return null;
    }

    FieldCondition? fieldCondition = null;

    if (range.DataType == VectorPayloadDataType.Datetime)
    {
        fieldCondition = new FieldCondition { Key = range.Key, DatetimeRange = new() };

        foreach (var condition in range.Conditions)
        {
            if (!DateTime.TryParse(condition.Value, out var dt))
            {
                continue;
            }

            var utc = dt.ToUniversalTime();
            var seconds = new DateTimeOffset(utc).ToUnixTimeSeconds();
            var nanos = (int)((utc.Ticks % TimeSpan.TicksPerSecond) * 100);
            var timestamp = new Google.Protobuf.WellKnownTypes.Timestamp { Seconds = seconds, Nanos = nanos };

            switch (condition.Operator.ToLower())
            {
                case "lt":
                    fieldCondition.DatetimeRange.Lt = timestamp;
                    break;
                case "lte":
                    fieldCondition.DatetimeRange.Lte = timestamp;
                    break;
                case "gt":
                    fieldCondition.DatetimeRange.Gt = timestamp;
                    break;
                case "gte":
                    fieldCondition.DatetimeRange.Gte = timestamp;
                    break;
            }
        }
    }
    else if (range.DataType == VectorPayloadDataType.Integer
        || range.DataType == VectorPayloadDataType.Double)
    {
        fieldCondition = new FieldCondition { Key = range.Key, Range = new() };

        foreach (var condition in range.Conditions)
        {
            if (!double.TryParse(condition.Value, out var doubleVal))
            {
                continue;
            }

            switch (condition.Operator.ToLower())
            {
                case "lt":
                    fieldCondition.Range.Lt = doubleVal;
                    break;
                case "lte":
                    fieldCondition.Range.Lte = doubleVal;
                    break;
                case "gt":
                    fieldCondition.Range.Gt = doubleVal;
                    break;
                case "gte":
                    fieldCondition.Range.Gte = doubleVal;
                    break;
            }
        }
Over-Filtering Risk

FilterFunctions uses regex word extraction and exact name matching to include functions. This may erroneously hide needed functions when names aren’t present verbatim in instructions, or include false positives from common words matching function names. Consider safer heuristics or fallbacks when Auto mode yields empty results.

public IEnumerable<FunctionDef> FilterFunctions(string instruction, Agent agent, StringComparer? comparer = null)
{
    var functions = agent.Functions.Concat(agent.SecondaryFunctions ?? []);
    if (agent.FuncVisMode.IsEqualTo(AgentFuncVisMode.Auto) && !string.IsNullOrWhiteSpace(instruction))
    {
        functions = FilterFunctions(instruction, functions, comparer);
    }
    return functions;
}

public IEnumerable<FunctionDef> FilterFunctions(string instruction, IEnumerable<FunctionDef> functions, StringComparer? comparer = null)
{
    comparer = comparer ?? StringComparer.OrdinalIgnoreCase;
    var matches = Regex.Matches(instruction, @"\b[A-Za-z0-9_-]+\b");
    var words = new HashSet<string>(matches.Select(m => m.Value), comparer);
    return functions.Where(x => words.Contains(x.Name, comparer));
}
Null Handling

MapPayload and places referencing x.Payload assume presence; ensure MapField and Value conversion covers arrays, nested objects, and nulls. Unknown kinds map to Unknown with empty string; verify consumers tolerate that and that previous supported kinds (e.g., lists) aren’t needed.

private Dictionary<string, VectorPayloadValue> MapPayload(MapField<string, Value>? payload)
{
    return payload?.ToDictionary(p => p.Key, p => p.Value.KindCase switch
    {
        Value.KindOneofCase.StringValue => VectorPayloadValue.BuildStringValue(p.Value.StringValue),
        Value.KindOneofCase.BoolValue => VectorPayloadValue.BuildBooleanValue(p.Value.BoolValue),
        Value.KindOneofCase.IntegerValue => VectorPayloadValue.BuildIntegerValue(p.Value.IntegerValue),
        Value.KindOneofCase.DoubleValue => VectorPayloadValue.BuildDoubleValue(p.Value.DoubleValue),
        _ => VectorPayloadValue.BuildUnkownValue(string.Empty)
    }) ?? [];
}
#endregion

@qodo-merge-pro
Copy link

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Function filtering risks overblocking

The Auto function filtering relies on naive regex word matching against
instruction text, which can easily miss valid functions (e.g., camelCase,
spaces, synonyms) or include false positives, breaking tool availability across
all providers. Consider a more robust selection strategy (e.g., explicit
tags/keywords per function, semantic similarity to instruction/context, or
LLM-assisted tool selection hints) and provide a safe fallback (e.g., minimum
whitelist) when no matches are found.

Examples:

src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs [132-137]
    public IEnumerable<FunctionDef> FilterFunctions(string instruction, IEnumerable<FunctionDef> functions, StringComparer? comparer = null)
    {
        comparer = comparer ?? StringComparer.OrdinalIgnoreCase;
        var matches = Regex.Matches(instruction, @"\b[A-Za-z0-9_-]+\b");
        var words = new HashSet<string>(matches.Select(m => m.Value), comparer);
        return functions.Where(x => words.Contains(x.Name, comparer));

Solution Walkthrough:

Before:

// In AgentService.Rendering.cs
public IEnumerable<FunctionDef> FilterFunctions(string instruction, IEnumerable<FunctionDef> functions, ...)
{
    // Extracts words like "get", "weather" from "get weather"
    var matches = Regex.Matches(instruction, @"\b[A-Za-z0-9_-]+\b");
    var words = new HashSet<string>(matches.Select(m => m.Value));

    // Only keeps functions if their name is an exact word in the instruction.
    // e.g., A function named "getWeather" will NOT be found.
    // A function named "get_weather" will be found.
    return functions.Where(x => words.Contains(x.Name));
}

After:

// In AgentService.Rendering.cs - A more robust approach
public IEnumerable<FunctionDef> FilterFunctions(string instruction, IEnumerable<FunctionDef> functions, ...)
{
    // Option 1: Use explicit keywords/tags defined on the function
    return functions.Where(func => 
        func.Keywords.Any(kw => instruction.Contains(kw, StringComparison.OrdinalIgnoreCase))
    );

    // Option 2 (more advanced): Use semantic similarity
    var instructionEmbedding = _embeddingService.GetVector(instruction);
    return functions.OrderByDescending(func => 
        CosineSimilarity(instructionEmbedding, func.Embedding)
    ).Take(5); // Take top N relevant functions
}
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical design flaw in the new auto function filtering logic, which uses a naive regex that will likely fail to select correct functions, impacting all LLM providers.

High
Possible issue
Use correct Qdrant range field types

Ensure BuildRangeFieldCondition sets the correct numeric range type for
integer-only comparisons. When range.DataType is Integer, populate IntegerRange
instead of generic Range to avoid Qdrant rejecting or misinterpreting filters.

src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs [698-710]

 private Condition? BuildRangeCondition(VectorFilterRange range)
 {
     var fieldCondition = BuildRangeFieldCondition(range);
     if (fieldCondition == null)
     {
         return null;
     }
 
     return new Condition
     {
         Field = fieldCondition
     };
 }
 
+private FieldCondition? BuildRangeFieldCondition(VectorFilterRange range)
+{
+    if (string.IsNullOrEmpty(range.Key) || range.Conditions.IsNullOrEmpty())
+    {
+        return null;
+    }
+
+    FieldCondition? fieldCondition = null;
+
+    if (range.DataType == VectorPayloadDataType.Datetime)
+    {
+        fieldCondition = new FieldCondition { Key = range.Key, DatetimeRange = new() };
+        foreach (var condition in range.Conditions)
+        {
+            if (!DateTime.TryParse(condition.Value, out var dt)) continue;
+            var utc = dt.ToUniversalTime();
+            var seconds = new DateTimeOffset(utc).ToUnixTimeSeconds();
+            var nanos = (int)((utc.Ticks % TimeSpan.TicksPerSecond) * 100);
+            var timestamp = new Google.Protobuf.WellKnownTypes.Timestamp { Seconds = seconds, Nanos = nanos };
+            switch (condition.Operator.ToLower())
+            {
+                case "lt":  fieldCondition.DatetimeRange.Lt = timestamp;  break;
+                case "lte": fieldCondition.DatetimeRange.Lte = timestamp; break;
+                case "gt":  fieldCondition.DatetimeRange.Gt = timestamp;  break;
+                case "gte": fieldCondition.DatetimeRange.Gte = timestamp; break;
+            }
+        }
+    }
+    else if (range.DataType == VectorPayloadDataType.Integer)
+    {
+        fieldCondition = new FieldCondition { Key = range.Key, IntegerRange = new() };
+        foreach (var condition in range.Conditions)
+        {
+            if (!long.TryParse(condition.Value, out var longVal)) continue;
+            switch (condition.Operator.ToLower())
+            {
+                case "lt":  fieldCondition.IntegerRange.Lt = longVal;  break;
+                case "lte": fieldCondition.IntegerRange.Lte = longVal; break;
+                case "gt":  fieldCondition.IntegerRange.Gt = longVal;  break;
+                case "gte": fieldCondition.IntegerRange.Gte = longVal; break;
+            }
+        }
+    }
+    else // Double/Float fallback
+    {
+        fieldCondition = new FieldCondition { Key = range.Key, Range = new() };
+        foreach (var condition in range.Conditions)
+        {
+            if (!double.TryParse(condition.Value, out var doubleVal)) continue;
+            switch (condition.Operator.ToLower())
+            {
+                case "lt":  fieldCondition.Range.Lt = doubleVal;  break;
+                case "lte": fieldCondition.Range.Lte = doubleVal; break;
+                case "gt":  fieldCondition.Range.Gt = doubleVal;  break;
+                case "gte": fieldCondition.Range.Gte = doubleVal; break;
+            }
+        }
+    }
+
+    return fieldCondition;
+}
+
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly points out that the implementation for range filtering on Integer types was missing, which would cause incorrect filter behavior for integer fields.

Medium
Handle empty instruction in filtering

Guard against empty or null instruction to avoid returning an empty function set
unexpectedly. When no instruction is provided, default to returning the original
functions to preserve functionality. This prevents inadvertent tool loss and
downstream null/empty iterations.

src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs [132-138]

 public IEnumerable<FunctionDef> FilterFunctions(string instruction, IEnumerable<FunctionDef> functions, StringComparer? comparer = null)
 {
-    comparer = comparer ?? StringComparer.OrdinalIgnoreCase;
+    if (string.IsNullOrWhiteSpace(instruction))
+    {
+        return functions ?? Enumerable.Empty<FunctionDef>();
+    }
+    comparer ??= StringComparer.OrdinalIgnoreCase;
     var matches = Regex.Matches(instruction, @"\b[A-Za-z0-9_-]+\b");
     var words = new HashSet<string>(matches.Select(m => m.Value), comparer);
-    return functions.Where(x => words.Contains(x.Name, comparer));
+    return (functions ?? Enumerable.Empty<FunctionDef>()).Where(x => !string.IsNullOrEmpty(x.Name) && words.Contains(x.Name, comparer));
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that an empty instruction would incorrectly filter out all functions and provides a robust fix to prevent this unintended behavior.

Medium
  • More

@iceljc iceljc changed the title Features/add agent function visibility Add agent function visibility and init web search Aug 19, 2025
@iceljc iceljc merged commit 524c7ff into SciSharp:master Aug 20, 2025
4 checks passed
@@ -0,0 +1,58 @@
using BotSharp.Abstraction.VectorStorage.Enums;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can utilize implict operator to simplier the syntax.
Define the implict opertor:

    public static implicit operator byte(Digit d) => d.digit;
    public static explicit operator VectorPayloadValue(byte b) => new VectorPayloadValue(b);

Can be used like:

        var d = new VectorPayloadValue(7);
        byte number = d;

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.

3 participants