Compare commits
2 Commits
5740d9126a
...
d78abd0c4d
| Author | SHA1 | Date | |
|---|---|---|---|
| d78abd0c4d | |||
| 97c1c309b1 |
@@ -15,6 +15,7 @@ using Polly.Registry;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using NexusReader.Infrastructure.Configuration;
|
using NexusReader.Infrastructure.Configuration;
|
||||||
using Qdrant.Client;
|
using Qdrant.Client;
|
||||||
|
using Qdrant.Client.Grpc;
|
||||||
using Neo4j.Driver;
|
using Neo4j.Driver;
|
||||||
|
|
||||||
namespace NexusReader.Infrastructure.Services;
|
namespace NexusReader.Infrastructure.Services;
|
||||||
@@ -285,6 +286,98 @@ public class KnowledgeService : IKnowledgeService
|
|||||||
_logger.LogWarning("[KnowledgeService] Skipping invalid link {Source} -> {Target}: one or both units are missing.", linkDto.Source, linkDto.Target);
|
_logger.LogWarning("[KnowledgeService] Skipping invalid link {Source} -> {Target}: one or both units are missing.", linkDto.Source, linkDto.Target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate and upsert vectors to Qdrant in batch
|
||||||
|
var unitsToEmbed = packet.Units
|
||||||
|
.Where(u => !string.IsNullOrEmpty(u.Content))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (unitsToEmbed.Any())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var contents = unitsToEmbed.Select(u => u.Content).ToList();
|
||||||
|
|
||||||
|
var embeddingResponse = await _retryPipeline.ExecuteAsync(async ct =>
|
||||||
|
await _embeddingGenerator.GenerateAsync(
|
||||||
|
contents,
|
||||||
|
new EmbeddingGenerationOptions { Dimensions = 768 },
|
||||||
|
cancellationToken: ct), cancellationToken);
|
||||||
|
|
||||||
|
var embeddings = embeddingResponse.ToList();
|
||||||
|
var points = new List<PointStruct>();
|
||||||
|
|
||||||
|
for (int i = 0; i < unitsToEmbed.Count; i++)
|
||||||
|
{
|
||||||
|
var unitDto = unitsToEmbed[i];
|
||||||
|
var vector = embeddings[i].Vector.ToArray();
|
||||||
|
|
||||||
|
var point = new PointStruct
|
||||||
|
{
|
||||||
|
Id = GetDeterministicGuid(unitDto.Id),
|
||||||
|
Vectors = vector,
|
||||||
|
Payload =
|
||||||
|
{
|
||||||
|
["content"] = unitDto.Content,
|
||||||
|
["type"] = unitDto.Type ?? string.Empty,
|
||||||
|
["tenantId"] = tenantId,
|
||||||
|
["ebookId"] = ebookId?.ToString() ?? string.Empty,
|
||||||
|
["metadataJson"] = JsonSerializer.Serialize(unitDto.Metadata)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
points.Add(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (points.Any())
|
||||||
|
{
|
||||||
|
await EnsureCollectionExistsAsync("knowledge_units", cancellationToken);
|
||||||
|
await _qdrantClient.UpsertAsync("knowledge_units", points, cancellationToken: cancellationToken);
|
||||||
|
_logger.LogInformation("[KnowledgeService] Successfully upserted {Count} points to Qdrant collection 'knowledge_units'.", points.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "[KnowledgeService] Failed to generate and upsert embeddings for knowledge units to Qdrant.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureCollectionExistsAsync(string collectionName, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var exists = await _qdrantClient.CollectionExistsAsync(collectionName, cancellationToken);
|
||||||
|
if (!exists)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[KnowledgeService] Creating Qdrant collection '{CollectionName}'...", collectionName);
|
||||||
|
await _qdrantClient.CreateCollectionAsync(
|
||||||
|
collectionName: collectionName,
|
||||||
|
vectorsConfig: new VectorParams
|
||||||
|
{
|
||||||
|
Size = 768,
|
||||||
|
Distance = Distance.Cosine
|
||||||
|
},
|
||||||
|
cancellationToken: cancellationToken
|
||||||
|
);
|
||||||
|
_logger.LogInformation("[KnowledgeService] Qdrant collection '{CollectionName}' created successfully.", collectionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "[KnowledgeService] Error ensuring Qdrant collection '{CollectionName}' exists.", collectionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Guid GetDeterministicGuid(string input)
|
||||||
|
{
|
||||||
|
if (Guid.TryParse(input, out var guid))
|
||||||
|
{
|
||||||
|
return guid;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var md5 = System.Security.Cryptography.MD5.Create();
|
||||||
|
byte[] hash = md5.ComputeHash(System.Text.Encoding.UTF8.GetBytes(input));
|
||||||
|
return new Guid(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<GroundednessResult>> VerifyGroundednessAsync(string answer, string context, string tenantId, CancellationToken cancellationToken = default)
|
public async Task<Result<GroundednessResult>> VerifyGroundednessAsync(string answer, string context, string tenantId, CancellationToken cancellationToken = default)
|
||||||
@@ -354,6 +447,7 @@ public class KnowledgeService : IKnowledgeService
|
|||||||
List<Qdrant.Client.Grpc.ScoredPoint> searchResult;
|
List<Qdrant.Client.Grpc.ScoredPoint> searchResult;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
await EnsureCollectionExistsAsync("knowledge_units", cancellationToken);
|
||||||
var response = await _qdrantClient.SearchAsync(
|
var response = await _qdrantClient.SearchAsync(
|
||||||
collectionName: "knowledge_units",
|
collectionName: "knowledge_units",
|
||||||
vector: queryVector,
|
vector: queryVector,
|
||||||
@@ -417,6 +511,7 @@ public class KnowledgeService : IKnowledgeService
|
|||||||
List<Qdrant.Client.Grpc.ScoredPoint> searchResult;
|
List<Qdrant.Client.Grpc.ScoredPoint> searchResult;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
await EnsureCollectionExistsAsync("knowledge_units", cancellationToken);
|
||||||
var response = await _qdrantClient.SearchAsync(
|
var response = await _qdrantClient.SearchAsync(
|
||||||
collectionName: "knowledge_units",
|
collectionName: "knowledge_units",
|
||||||
vector: queryVector,
|
vector: queryVector,
|
||||||
@@ -602,6 +697,7 @@ public class KnowledgeService : IKnowledgeService
|
|||||||
List<Qdrant.Client.Grpc.ScoredPoint> searchResult;
|
List<Qdrant.Client.Grpc.ScoredPoint> searchResult;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
await EnsureCollectionExistsAsync("knowledge_units", cancellationToken);
|
||||||
var response = await _qdrantClient.SearchAsync(
|
var response = await _qdrantClient.SearchAsync(
|
||||||
collectionName: "knowledge_units",
|
collectionName: "knowledge_units",
|
||||||
vector: queryVector,
|
vector: queryVector,
|
||||||
@@ -790,6 +886,16 @@ Strict Grounding Rules:
|
|||||||
await dbContext.SemanticKnowledgeCache.ExecuteDeleteAsync(cancellationToken);
|
await dbContext.SemanticKnowledgeCache.ExecuteDeleteAsync(cancellationToken);
|
||||||
await dbContext.KnowledgeUnits.ExecuteDeleteAsync(cancellationToken);
|
await dbContext.KnowledgeUnits.ExecuteDeleteAsync(cancellationToken);
|
||||||
await dbContext.KnowledgeUnitLinks.ExecuteDeleteAsync(cancellationToken);
|
await dbContext.KnowledgeUnitLinks.ExecuteDeleteAsync(cancellationToken);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _qdrantClient.DeleteCollectionAsync("knowledge_units", cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "[KnowledgeService] Failed to drop Qdrant collection 'knowledge_units' during cache clear.");
|
||||||
|
}
|
||||||
|
|
||||||
return Result.Ok();
|
return Result.Ok();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
margin: 1.5rem auto;
|
margin: 1.5rem auto;
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--nexus-font-sans), 'Inter', sans-serif;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,19 +11,21 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #121212;
|
background: rgba(255, 255, 255, 0.03);
|
||||||
border: 1px solid rgba(138, 43, 226, 0.4); /* Neon fiolet border on blur */
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 0.65rem 1.1rem;
|
padding: 0.65rem 1.1rem;
|
||||||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5), 0 0 8px rgba(138, 43, 226, 0.1);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Focused state: glowing neon green border */
|
/* Focused state: glowing neon border matching other dashboard components */
|
||||||
.nexus-search-container.focused .search-wrapper {
|
.nexus-search-container.focused .search-wrapper {
|
||||||
background-color: #181818;
|
background: rgba(255, 255, 255, 0.05);
|
||||||
border-color: #00ff7f; /* Neon green focus */
|
border-color: var(--nexus-neon);
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6), 0 0 18px rgba(0, 255, 127, 0.35);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5), 0 0 15px rgba(0, 255, 153, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-icon-container {
|
.search-icon-container {
|
||||||
@@ -42,7 +44,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nexus-search-container.focused .nexus-icon {
|
.nexus-search-container.focused .nexus-icon {
|
||||||
color: #00ff7f;
|
color: var(--nexus-neon);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nexus-search-input {
|
.nexus-search-input {
|
||||||
@@ -77,11 +79,11 @@
|
|||||||
.ai-pulse-dot {
|
.ai-pulse-dot {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
background-color: #00ff7f;
|
background-color: var(--nexus-neon);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 0 8px #00ff7f;
|
box-shadow: 0 0 8px var(--nexus-neon);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-pulse-dot::after {
|
.ai-pulse-dot::after {
|
||||||
@@ -91,7 +93,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
background-color: #00ff7f;
|
background-color: var(--nexus-neon);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
animation: pulse 2s infinite ease-in-out;
|
animation: pulse 2s infinite ease-in-out;
|
||||||
@@ -120,17 +122,17 @@
|
|||||||
top: calc(100% + 8px);
|
top: calc(100% + 8px);
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background: rgba(18, 18, 18, 0.82);
|
background: rgba(18, 18, 18, 0.9);
|
||||||
backdrop-filter: blur(18px) saturate(160%);
|
backdrop-filter: blur(20px) saturate(160%);
|
||||||
-webkit-backdrop-filter: blur(18px) saturate(160%);
|
-webkit-backdrop-filter: blur(20px) saturate(160%);
|
||||||
border: 1px solid rgba(138, 43, 226, 0.35);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.7), 0 0 20px rgba(138, 43, 226, 0.15);
|
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.6), 0 0 20px rgba(0, 255, 153, 0.05);
|
||||||
max-height: 420px;
|
max-height: 420px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: rgba(138, 43, 226, 0.4) transparent;
|
scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
|
||||||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
animation: slideDown 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
animation: slideDown 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
@@ -140,21 +142,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search-dropdown::-webkit-scrollbar-thumb {
|
.search-dropdown::-webkit-scrollbar-thumb {
|
||||||
background: rgba(138, 43, 226, 0.4);
|
background: rgba(255, 255, 255, 0.15);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-dropdown::-webkit-scrollbar-thumb:hover {
|
.search-dropdown::-webkit-scrollbar-thumb:hover {
|
||||||
background: rgba(0, 255, 127, 0.5);
|
background: var(--nexus-neon);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* In-flight spinners */
|
/* In-flight spinners */
|
||||||
.neon-spinner {
|
.neon-spinner {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
border: 2px solid rgba(0, 255, 127, 0.15);
|
border: 2px solid rgba(0, 255, 153, 0.15);
|
||||||
border-top: 2px solid #00ff7f;
|
border-top: 2px solid var(--nexus-neon);
|
||||||
border-right: 2px solid #8a2be2;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: spin 0.75s linear infinite;
|
animation: spin 0.75s linear infinite;
|
||||||
}
|
}
|
||||||
@@ -162,9 +163,8 @@
|
|||||||
.neon-spinner-large {
|
.neon-spinner-large {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border: 3px solid rgba(138, 43, 226, 0.15);
|
border: 3px solid rgba(255, 255, 255, 0.05);
|
||||||
border-top: 3px solid #8a2be2;
|
border-top: 3px solid var(--nexus-neon);
|
||||||
border-right: 3px solid #00ff7f;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: spin 0.8s cubic-bezier(0.5, 0.1, 0.5, 0.9) infinite;
|
animation: spin 0.8s cubic-bezier(0.5, 0.1, 0.5, 0.9) infinite;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
@@ -217,8 +217,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.result-card:hover {
|
.result-card:hover {
|
||||||
background: rgba(138, 43, 226, 0.08);
|
background: rgba(0, 255, 153, 0.05);
|
||||||
border-color: rgba(138, 43, 226, 0.35);
|
border-color: rgba(0, 255, 153, 0.2);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
@@ -232,14 +232,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.relevance-badge {
|
.relevance-badge {
|
||||||
background: rgba(0, 255, 127, 0.15);
|
background: rgba(0, 255, 153, 0.1);
|
||||||
color: #00ff7f;
|
color: var(--nexus-neon);
|
||||||
border: 1px solid rgba(0, 255, 127, 0.3);
|
border: 1px solid rgba(0, 255, 153, 0.25);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 0.15rem 0.45rem;
|
padding: 0.15rem 0.45rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
text-shadow: 0 0 4px rgba(0, 255, 127, 0.3);
|
text-shadow: 0 0 4px rgba(0, 255, 153, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.source-title {
|
.source-title {
|
||||||
@@ -267,9 +267,9 @@
|
|||||||
|
|
||||||
/* Markup highlights */
|
/* Markup highlights */
|
||||||
::deep mark.search-highlight {
|
::deep mark.search-highlight {
|
||||||
background: rgba(0, 255, 127, 0.22);
|
background: rgba(0, 255, 153, 0.2);
|
||||||
color: #00ff7f;
|
color: var(--nexus-neon);
|
||||||
border-bottom: 1px solid rgba(0, 255, 127, 0.55);
|
border-bottom: 1px solid var(--nexus-neon);
|
||||||
padding: 0.05rem 0.15rem;
|
padding: 0.05rem 0.15rem;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -279,15 +279,15 @@
|
|||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
box-shadow: 0 0 0 0 rgba(0, 255, 127, 0.7);
|
box-shadow: 0 0 0 0 rgba(0, 255, 153, 0.7);
|
||||||
}
|
}
|
||||||
70% {
|
70% {
|
||||||
transform: scale(1.6);
|
transform: scale(1.6);
|
||||||
box-shadow: 0 0 0 6px rgba(0, 255, 127, 0);
|
box-shadow: 0 0 0 6px rgba(0, 255, 153, 0);
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
box-shadow: 0 0 0 0 rgba(0, 255, 127, 0);
|
box-shadow: 0 0 0 0 rgba(0, 255, 153, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user