microsoft/teams.net

Public

mirrored fromhttps://github.com/microsoft/teams.netAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feature/pabot-httpcontext-botid

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

core/samples/ExtAIBot/CitationCollector.cs

110lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System.Text.Json;
5using System.Text.RegularExpressions;
6using Microsoft.Teams.Apps.Schema.Entities;
7
8namespace ExtAIBot;
9
10// Parses MCP search results as they are returned by tools and accumulates citation metadata.
11// After streaming completes, BuildEntities() returns Teams CitationEntity objects for any
12// [N] references that appear in the final response text.
13sealed class CitationCollector
14{
15 private readonly ILogger _logger;
16 private readonly Dictionary<string, CitationEntry> _citations = [];
17
18 public CitationCollector(ILogger logger)
19 {
20 _logger = logger;
21 }
22
23 public void TryExtract(string result)
24 {
25 try
26 {
27 using JsonDocument doc = JsonDocument.Parse(result);
28 if (!TryFindResults(doc.RootElement, out JsonElement results)) return;
29
30 foreach (JsonElement item in results.EnumerateArray())
31 {
32 string? url = GetString(item, "contentUrl") ?? GetString(item, "link");
33 if (url is null || _citations.ContainsKey(url)) continue;
34
35 string snippet = GetString(item, "content") ?? GetString(item, "description") ?? "";
36 _citations[url] = new CitationEntry(
37 Position: _citations.Count + 1,
38 Url: url,
39 Title: GetString(item, "title") ?? "",
40 Snippet: snippet.Length > 160 ? snippet[..160] : snippet);
41 }
42 }
43 catch (JsonException ex)
44 {
45 _logger.LogDebug(ex, "Skipped citation extraction because the tool result was not valid JSON.");
46 }
47 catch (FormatException ex)
48 {
49 _logger.LogDebug(ex, "Skipped citation extraction because the tool result had an unexpected format.");
50 }
51 }
52
53 public IList<Entity> BuildEntities(string fullText)
54 {
55 HashSet<int> used = [];
56 foreach (Match match in Regex.Matches(fullText, @"\[(\d+)\]"))
57 {
58 if (int.TryParse(match.Groups[1].Value, out int position))
59 used.Add(position);
60 }
61
62 List<CitationClaim> claims = [.. _citations.Values
63 .Where(e => used.Contains(e.Position))
64 .Select(e => new CitationClaim
65 {
66 Position = e.Position,
67 Appearance = new CitationAppearance
68 {
69 Name = string.IsNullOrEmpty(e.Title)
70 ? $"Source {e.Position}"
71 : e.Title[..Math.Min(80, e.Title.Length)],
72 Abstract = string.IsNullOrEmpty(e.Snippet)
73 ? "No description available."
74 : e.Snippet,
75 Url = Uri.TryCreate(e.Url, UriKind.Absolute, out Uri? uri) ? uri : null
76 }.ToDocument()
77 })];
78
79 // TODO : work on Add Citations/feedback/AI label etc in builder
80 return claims.Count == 0
81 ? [new OMessageEntity { AdditionalType = ["AIGeneratedContent"] }]
82 : [new CitationEntity { AdditionalType = ["AIGeneratedContent"], Citation = claims }];
83 }
84
85 // MCP InvokeAsync returns a JsonElement of CallToolResult, not the raw server JSON.
86 // Results may be at root or nested one level deep (e.g. CallToolResult.structuredContent.results).
87 private static bool TryFindResults(JsonElement element, out JsonElement results)
88 {
89 if (element.TryGetProperty("results", out results) && results.ValueKind == JsonValueKind.Array)
90 return true;
91
92 foreach (JsonProperty prop in element.EnumerateObject())
93 {
94 if (prop.Value.ValueKind == JsonValueKind.Object &&
95 prop.Value.TryGetProperty("results", out results) &&
96 results.ValueKind == JsonValueKind.Array)
97 return true;
98 }
99
100 results = default;
101 return false;
102 }
103
104 private static string? GetString(JsonElement el, string property) =>
105 el.TryGetProperty(property, out JsonElement v) && v.ValueKind == JsonValueKind.String
106 ? v.GetString()
107 : null;
108}
109
110sealed record CitationEntry(int Position, string Url, string Title, string Snippet);
111