Ship Assistent disk persist, park LLM, and memory UI cleanup.
Split the extension into partials, persist chats on the data volume, park/warm the chat model around Generate, and drop dual raw/persona dump paths. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+393
-40
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@@ -14,10 +15,31 @@ namespace Mrleo1nid.SwarmAssistent;
|
||||
/// <summary>Local SQLite vector memory with Ollama /api/embed.
|
||||
/// Two layers: shared (persona='') is visible to every personality; personal (persona=id)
|
||||
/// is not written back to shared. On retrieve, personal overwrites shared on the same kind+key.</summary>
|
||||
public sealed class AssistentMemory : IDisposable
|
||||
public sealed partial class AssistentMemory : IDisposable
|
||||
{
|
||||
public const string SharedPersona = "";
|
||||
|
||||
public sealed class RetrieveOptions
|
||||
{
|
||||
public int TopK { get; set; } = 10;
|
||||
public float MinScore { get; set; } = 0.32f;
|
||||
public string KindFilter { get; set; }
|
||||
public IReadOnlyDictionary<string, int> Quotas { get; set; }
|
||||
public bool ApplyQuotas { get; set; } = true;
|
||||
}
|
||||
|
||||
static readonly Dictionary<string, int> DefaultQuotas = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["card"] = 3,
|
||||
["lora"] = 3,
|
||||
["pitfall"] = 3,
|
||||
["path"] = 2,
|
||||
["note"] = 4,
|
||||
["model"] = 2,
|
||||
["aspect"] = 1,
|
||||
};
|
||||
|
||||
readonly string _dataRoot;
|
||||
readonly string _dbPath;
|
||||
readonly HttpClient _http;
|
||||
readonly object _lock = new();
|
||||
@@ -28,7 +50,8 @@ public sealed class AssistentMemory : IDisposable
|
||||
|
||||
public AssistentMemory(string dataRoot, HttpClient http, string defaultEmbedModel = "nomic-embed-text")
|
||||
{
|
||||
string dir = Path.Combine(dataRoot ?? ".", "Assistent", "memory");
|
||||
_dataRoot = string.IsNullOrWhiteSpace(dataRoot) ? "." : dataRoot;
|
||||
string dir = Path.Combine(_dataRoot, "Assistent", "memory");
|
||||
Directory.CreateDirectory(dir);
|
||||
_dbPath = Path.Combine(dir, "assistent.sqlite");
|
||||
_http = http;
|
||||
@@ -63,6 +86,8 @@ public sealed class AssistentMemory : IDisposable
|
||||
}
|
||||
_conn = new SqliteConnection($"Data Source={_dbPath}");
|
||||
_conn.Open();
|
||||
TryPragma("journal_mode=WAL");
|
||||
TryPragma("busy_timeout=5000");
|
||||
using (SqliteCommand cmd = _conn.CreateCommand())
|
||||
{
|
||||
cmd.CommandText =
|
||||
@@ -96,6 +121,23 @@ public sealed class AssistentMemory : IDisposable
|
||||
""";
|
||||
idx.ExecuteNonQuery();
|
||||
}
|
||||
EnsureFts();
|
||||
try
|
||||
{
|
||||
EnsureTagsSchema();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"AssistentMemory tags schema: {ex.Message}");
|
||||
}
|
||||
try
|
||||
{
|
||||
EnsureStoreSchema();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"AssistentMemory store schema: {ex.Message}");
|
||||
}
|
||||
_embedModel = GetMeta("embed_model") ?? _embedModel;
|
||||
_ = int.TryParse(GetMeta("dims"), out _dims);
|
||||
_ = int.TryParse(GetMeta("seed_version"), out _seedVersion);
|
||||
@@ -154,6 +196,89 @@ public sealed class AssistentMemory : IDisposable
|
||||
Logs.Debug("AssistentMemory: migrated sqlite to shared+personal persona column (existing rows → shared)");
|
||||
}
|
||||
|
||||
void TryPragma(string pragma)
|
||||
{
|
||||
try
|
||||
{
|
||||
using SqliteCommand cmd = _conn.CreateCommand();
|
||||
cmd.CommandText = "PRAGMA " + pragma;
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"AssistentMemory PRAGMA {pragma}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
bool TableExists(string name)
|
||||
{
|
||||
using SqliteCommand cmd = _conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT 1 FROM sqlite_master WHERE type IN ('table','view') AND name = $n LIMIT 1";
|
||||
cmd.Parameters.AddWithValue("$n", name);
|
||||
return cmd.ExecuteScalar() is not null;
|
||||
}
|
||||
|
||||
void Exec(string sql)
|
||||
{
|
||||
using SqliteCommand cmd = _conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
void EnsureFts()
|
||||
{
|
||||
try
|
||||
{
|
||||
Exec(
|
||||
"""
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
||||
key,
|
||||
text,
|
||||
tokenize = 'unicode61 remove_diacritics 2'
|
||||
);
|
||||
""");
|
||||
Exec(
|
||||
"""
|
||||
CREATE TRIGGER IF NOT EXISTS memories_fts_ai AFTER INSERT ON memories BEGIN
|
||||
INSERT INTO memories_fts(rowid, key, text) VALUES (new.id, new.key, new.text);
|
||||
END;
|
||||
""");
|
||||
Exec(
|
||||
"""
|
||||
CREATE TRIGGER IF NOT EXISTS memories_fts_ad AFTER DELETE ON memories BEGIN
|
||||
INSERT INTO memories_fts(memories_fts, rowid) VALUES('delete', old.id);
|
||||
END;
|
||||
""");
|
||||
Exec(
|
||||
"""
|
||||
CREATE TRIGGER IF NOT EXISTS memories_fts_au AFTER UPDATE ON memories BEGIN
|
||||
INSERT INTO memories_fts(memories_fts, rowid) VALUES('delete', old.id);
|
||||
INSERT INTO memories_fts(rowid, key, text) VALUES (new.id, new.key, new.text);
|
||||
END;
|
||||
""");
|
||||
int mem = 0, fts = 0;
|
||||
using (SqliteCommand c = _conn.CreateCommand())
|
||||
{
|
||||
c.CommandText = "SELECT COUNT(*) FROM memories";
|
||||
mem = Convert.ToInt32(c.ExecuteScalar());
|
||||
}
|
||||
using (SqliteCommand c = _conn.CreateCommand())
|
||||
{
|
||||
c.CommandText = "SELECT COUNT(*) FROM memories_fts";
|
||||
fts = Convert.ToInt32(c.ExecuteScalar());
|
||||
}
|
||||
if (mem != fts)
|
||||
{
|
||||
Exec("DELETE FROM memories_fts");
|
||||
Exec("INSERT INTO memories_fts(rowid, key, text) SELECT id, key, text FROM memories");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"AssistentMemory FTS5 unavailable, cosine-only: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
string GetMeta(string key)
|
||||
{
|
||||
using SqliteCommand cmd = _conn.CreateCommand();
|
||||
@@ -268,6 +393,7 @@ public sealed class AssistentMemory : IDisposable
|
||||
List<JObject> docs = config.LoadMemorySeedDocs();
|
||||
if (docs.Count == 0)
|
||||
{
|
||||
TryIndexTags();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -287,6 +413,7 @@ public sealed class AssistentMemory : IDisposable
|
||||
}
|
||||
if (!missing)
|
||||
{
|
||||
TryIndexTags();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -299,6 +426,7 @@ public sealed class AssistentMemory : IDisposable
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"AssistentMemory seed defer (embed unavailable): {ex.Message}");
|
||||
TryIndexTags();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -356,6 +484,7 @@ public sealed class AssistentMemory : IDisposable
|
||||
Logs.Debug($"AssistentMemory seed item {kind}/{key}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
TryIndexTags();
|
||||
}
|
||||
|
||||
public void Upsert(string kind, string key, string text, string source, JToken meta, float[] embedding, string persona = null)
|
||||
@@ -428,29 +557,8 @@ public sealed class AssistentMemory : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Retrieve shared + the given persona chain. Personal overwrites shared (and parent personas) on kind+key.</summary>
|
||||
public async Task<JArray> RetrieveAsync(string baseUrl, string query, int topK = 10, string modelOverride = null, IEnumerable<string> personaChain = null)
|
||||
static Dictionary<string, int> PersonaRankMap(IEnumerable<string> personaChain)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
lock (_lock)
|
||||
{
|
||||
EnsureOpen();
|
||||
}
|
||||
string model = string.IsNullOrWhiteSpace(modelOverride) ? _embedModel : modelOverride;
|
||||
float[] q;
|
||||
try
|
||||
{
|
||||
q = await EmbedAsync(baseUrl, model, query);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"AssistentMemory retrieve embed: {ex.Message}");
|
||||
return [];
|
||||
}
|
||||
|
||||
Dictionary<string, int> rank = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[SharedPersona] = 0,
|
||||
@@ -465,46 +573,219 @@ public sealed class AssistentMemory : IDisposable
|
||||
}
|
||||
rank[p] = i++;
|
||||
}
|
||||
return rank;
|
||||
}
|
||||
|
||||
List<(float score, int personaRank, int sourceRank, JObject row)> scored = [];
|
||||
static string BuildFtsMatch(string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
HashSet<string> tokens = new(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (Match m in Regex.Matches(query, @"[\p{L}\p{N}_-]{2,}"))
|
||||
{
|
||||
string t = m.Value.Replace("\"", "").Trim('_', '-');
|
||||
if (t.Length >= 2)
|
||||
{
|
||||
tokens.Add(t);
|
||||
}
|
||||
string spaced = t.Replace('_', ' ').Replace('-', ' ');
|
||||
if (!string.Equals(spaced, t, StringComparison.OrdinalIgnoreCase) && spaced.Length >= 2)
|
||||
{
|
||||
foreach (string p in spaced.Split(' ', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (p.Length >= 2)
|
||||
{
|
||||
tokens.Add(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tokens.Count >= 12)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tokens.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return string.Join(" OR ", tokens.Select(t => $"\"{t}\""));
|
||||
}
|
||||
|
||||
Dictionary<long, int> FtsRowRanks(string match, int limit)
|
||||
{
|
||||
Dictionary<long, int> ranks = [];
|
||||
if (string.IsNullOrWhiteSpace(match) || !TableExists("memories_fts"))
|
||||
{
|
||||
return ranks;
|
||||
}
|
||||
try
|
||||
{
|
||||
using SqliteCommand cmd = _conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT rowid FROM memories_fts WHERE memories_fts MATCH $q ORDER BY rank LIMIT $lim";
|
||||
cmd.Parameters.AddWithValue("$q", match);
|
||||
cmd.Parameters.AddWithValue("$lim", limit);
|
||||
using SqliteDataReader reader = cmd.ExecuteReader();
|
||||
int r = 0;
|
||||
while (reader.Read())
|
||||
{
|
||||
ranks[reader.GetInt64(0)] = r++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"AssistentMemory FTS match: {ex.Message}");
|
||||
}
|
||||
return ranks;
|
||||
}
|
||||
|
||||
static float KeyBoost(string query, string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
string q = query.Replace('_', ' ');
|
||||
string k = (key ?? "").Replace('_', ' ');
|
||||
if (k.Length >= 3 && q.Contains(k, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return 0.22f;
|
||||
}
|
||||
if (k.Length >= 3 && k.Contains(q.Trim(), StringComparison.OrdinalIgnoreCase) && q.Trim().Length >= 4)
|
||||
{
|
||||
return 0.12f;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static JArray ApplyQuotas(IEnumerable<JObject> ordered, int topK, IReadOnlyDictionary<string, int> quotas)
|
||||
{
|
||||
Dictionary<string, int> used = new(StringComparer.OrdinalIgnoreCase);
|
||||
List<JObject> picked = [];
|
||||
List<JObject> overflow = [];
|
||||
foreach (JObject row in ordered)
|
||||
{
|
||||
string kind = row["kind"]?.ToString() ?? "note";
|
||||
int cap = 2;
|
||||
if (quotas is not null && quotas.TryGetValue(kind, out int q))
|
||||
{
|
||||
cap = q;
|
||||
}
|
||||
else if (DefaultQuotas.TryGetValue(kind, out int d))
|
||||
{
|
||||
cap = d;
|
||||
}
|
||||
used.TryGetValue(kind, out int n);
|
||||
if (n < cap)
|
||||
{
|
||||
picked.Add(row);
|
||||
used[kind] = n + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
overflow.Add(row);
|
||||
}
|
||||
if (picked.Count >= topK)
|
||||
{
|
||||
return new JArray(picked);
|
||||
}
|
||||
}
|
||||
foreach (JObject row in overflow)
|
||||
{
|
||||
if (picked.Count >= topK)
|
||||
{
|
||||
break;
|
||||
}
|
||||
picked.Add(row);
|
||||
}
|
||||
return new JArray(picked);
|
||||
}
|
||||
|
||||
/// <summary>Retrieve shared + the given persona chain. Hybrid FTS + cosine; personal overwrites shared on kind+key.</summary>
|
||||
public async Task<JArray> RetrieveAsync(string baseUrl, string query, int topK = 10, string modelOverride = null, IEnumerable<string> personaChain = null, RetrieveOptions options = null)
|
||||
{
|
||||
options ??= new RetrieveOptions { TopK = topK };
|
||||
if (options.TopK <= 0)
|
||||
{
|
||||
options.TopK = topK;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
lock (_lock)
|
||||
{
|
||||
EnsureOpen();
|
||||
}
|
||||
string model = string.IsNullOrWhiteSpace(modelOverride) ? _embedModel : modelOverride;
|
||||
float[] qvec = null;
|
||||
try
|
||||
{
|
||||
qvec = await EmbedAsync(baseUrl, model, query);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"AssistentMemory retrieve embed: {ex.Message}");
|
||||
}
|
||||
|
||||
Dictionary<string, int> rank = PersonaRankMap(personaChain);
|
||||
string kindFilter = string.IsNullOrWhiteSpace(options.KindFilter) ? null : options.KindFilter.Trim().ToLowerInvariant();
|
||||
string ftsMatch = BuildFtsMatch(query);
|
||||
|
||||
List<(float hybrid, float cosine, int personaRank, int sourceRank, bool fts, JObject row)> scored = [];
|
||||
lock (_lock)
|
||||
{
|
||||
EnsureOpen();
|
||||
Dictionary<long, int> ftsRanks = FtsRowRanks(ftsMatch, Math.Max(40, options.TopK * 4));
|
||||
using SqliteCommand cmd = _conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT kind, key, text, source, meta_json, embedding, persona FROM memories WHERE embedding IS NOT NULL";
|
||||
cmd.CommandText = "SELECT id, kind, key, text, source, embedding, persona FROM memories";
|
||||
using SqliteDataReader reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
long id = reader.GetInt64(0);
|
||||
string kind = reader.GetString(1);
|
||||
if (kindFilter is not null && !string.Equals(kind, kindFilter, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
string persona = reader.IsDBNull(6) ? SharedPersona : reader.GetString(6) ?? SharedPersona;
|
||||
if (!rank.TryGetValue(persona, out int personaRank))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
float[] emb = BytesToFloats(reader.IsDBNull(5) ? null : (byte[])reader.GetValue(5));
|
||||
float score = Cosine(q, emb);
|
||||
if (float.IsNegativeInfinity(score))
|
||||
string key = reader.GetString(2);
|
||||
string text = reader.GetString(3);
|
||||
string source = reader.GetString(4);
|
||||
float cosine = 0;
|
||||
if (qvec is not null && !reader.IsDBNull(5))
|
||||
{
|
||||
continue;
|
||||
cosine = Cosine(qvec, BytesToFloats((byte[])reader.GetValue(5)));
|
||||
if (float.IsNegativeInfinity(cosine))
|
||||
{
|
||||
cosine = 0;
|
||||
}
|
||||
}
|
||||
string kind = reader.GetString(0);
|
||||
string key = reader.GetString(1);
|
||||
string source = reader.GetString(3);
|
||||
bool ftsHit = ftsRanks.TryGetValue(id, out int ftsRank);
|
||||
float ftsBoost = ftsHit ? 0.28f * (1f - (ftsRank / 50f)) : 0;
|
||||
float hybrid = cosine + ftsBoost + KeyBoost(query, key);
|
||||
bool shared = persona == SharedPersona;
|
||||
scored.Add((score, personaRank, SourceRank(source), new JObject
|
||||
scored.Add((hybrid, cosine, personaRank, SourceRank(source), ftsHit, new JObject
|
||||
{
|
||||
["kind"] = kind,
|
||||
["key"] = key,
|
||||
["text"] = reader.GetString(2),
|
||||
["text"] = text,
|
||||
["source"] = source,
|
||||
["scope"] = shared ? "shared" : "personal",
|
||||
["persona"] = shared ? "shared" : persona,
|
||||
["score"] = Math.Round(score, 4),
|
||||
["score"] = Math.Round(hybrid, 4),
|
||||
["cosine"] = Math.Round(cosine, 4),
|
||||
["fts"] = ftsHit,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Personal (and later parents) overwrite shared on the same kind+key; user beats bundled.
|
||||
Dictionary<string, (float score, int personaRank, int sourceRank, JObject row)> best = new(StringComparer.OrdinalIgnoreCase);
|
||||
Dictionary<string, (float hybrid, float cosine, int personaRank, int sourceRank, bool fts, JObject row)> best = new(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var item in scored)
|
||||
{
|
||||
string id = $"{item.row["kind"]}\n{item.row["key"]}";
|
||||
@@ -512,7 +793,7 @@ public sealed class AssistentMemory : IDisposable
|
||||
{
|
||||
if (item.personaRank < cur.personaRank
|
||||
|| (item.personaRank == cur.personaRank && item.sourceRank < cur.sourceRank)
|
||||
|| (item.personaRank == cur.personaRank && item.sourceRank == cur.sourceRank && item.score <= cur.score))
|
||||
|| (item.personaRank == cur.personaRank && item.sourceRank == cur.sourceRank && item.hybrid <= cur.hybrid))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -520,7 +801,79 @@ public sealed class AssistentMemory : IDisposable
|
||||
best[id] = item;
|
||||
}
|
||||
|
||||
return new JArray(best.Values.OrderByDescending(s => s.score).Take(Math.Clamp(topK, 1, 30)).Select(s => s.row));
|
||||
float min = options.MinScore;
|
||||
IEnumerable<JObject> ordered = best.Values
|
||||
.Where(s => s.fts || s.hybrid >= min || s.cosine >= min)
|
||||
.OrderByDescending(s => s.hybrid)
|
||||
.Select(s => s.row);
|
||||
|
||||
int k = Math.Clamp(options.TopK, 1, 30);
|
||||
if (options.ApplyQuotas)
|
||||
{
|
||||
return ApplyQuotas(ordered, k, options.Quotas ?? DefaultQuotas);
|
||||
}
|
||||
return new JArray(ordered.Take(k));
|
||||
}
|
||||
|
||||
/// <summary>Exact kind+key read with the same personal-over-shared overlay as retrieve.</summary>
|
||||
public JObject Get(string kind, string key, IEnumerable<string> personaChain = null)
|
||||
{
|
||||
kind = (kind ?? "note").Trim().ToLowerInvariant();
|
||||
key = (key ?? "").Trim();
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Dictionary<string, int> rank = PersonaRankMap(personaChain);
|
||||
JObject best = null;
|
||||
int bestPersona = -1, bestSource = -1;
|
||||
lock (_lock)
|
||||
{
|
||||
EnsureOpen();
|
||||
using SqliteCommand cmd = _conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT kind, key, text, source, persona, updated FROM memories WHERE kind = $kind AND key = $key";
|
||||
cmd.Parameters.AddWithValue("$kind", kind);
|
||||
cmd.Parameters.AddWithValue("$key", key);
|
||||
using SqliteDataReader reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
string persona = reader.IsDBNull(4) ? SharedPersona : reader.GetString(4) ?? SharedPersona;
|
||||
if (!rank.TryGetValue(persona, out int personaRank))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int src = SourceRank(reader.GetString(3));
|
||||
if (best is not null && (personaRank < bestPersona || (personaRank == bestPersona && src <= bestSource)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
bestPersona = personaRank;
|
||||
bestSource = src;
|
||||
bool shared = persona == SharedPersona;
|
||||
best = new JObject
|
||||
{
|
||||
["kind"] = reader.GetString(0),
|
||||
["key"] = reader.GetString(1),
|
||||
["text"] = reader.GetString(2),
|
||||
["source"] = reader.GetString(3),
|
||||
["scope"] = shared ? "shared" : "personal",
|
||||
["persona"] = shared ? "shared" : persona,
|
||||
["updated"] = reader.IsDBNull(5) ? 0 : reader.GetInt64(5),
|
||||
};
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
public async Task<JArray> SearchAsync(string baseUrl, string query, string kind, int topK, string modelOverride, IEnumerable<string> personaChain)
|
||||
{
|
||||
return await RetrieveAsync(baseUrl, query, topK, modelOverride, personaChain, new RetrieveOptions
|
||||
{
|
||||
TopK = Math.Clamp(topK, 1, 30),
|
||||
MinScore = 0.18f,
|
||||
KindFilter = kind,
|
||||
ApplyQuotas = false,
|
||||
});
|
||||
}
|
||||
|
||||
public JArray ListAll(int limit = 200)
|
||||
|
||||
Reference in New Issue
Block a user