Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b5d1135
feat: define approved knowledge contract
YangYuS8 Jul 16, 2026
a46edc2
feat: enforce knowledge approval and tenant isolation
YangYuS8 Jul 16, 2026
11ab32d
test: enforce knowledge trust boundary
YangYuS8 Jul 16, 2026
3123d92
feat: add VikingDB approved knowledge adapter
YangYuS8 Jul 16, 2026
0450c22
test: verify VikingDB filters and provenance mapping
YangYuS8 Jul 16, 2026
d39a95c
feat: expose private approved knowledge API
YangYuS8 Jul 16, 2026
8b0901b
fix: simplify approved knowledge HTTP boundary
YangYuS8 Jul 16, 2026
79b9d9b
test: protect approved knowledge HTTP service
YangYuS8 Jul 16, 2026
e2e684e
feat: enforce service-level score floor
YangYuS8 Jul 16, 2026
f52fc7b
feat: add private VikingDB knowledge service
YangYuS8 Jul 16, 2026
2545f1d
docs: define approved VikingDB knowledge pipeline
YangYuS8 Jul 16, 2026
d7e4d6d
deploy: add hardened knowledge service unit
YangYuS8 Jul 16, 2026
2d7148e
docs: add approved knowledge environment template
YangYuS8 Jul 16, 2026
5d202ec
feat: add deterministic knowledge retrieval evaluation
YangYuS8 Jul 16, 2026
e3b58a6
test: verify retrieval evaluation gates
YangYuS8 Jul 16, 2026
2e764b6
feat: add knowledge retrieval benchmark CLI
YangYuS8 Jul 16, 2026
04dd051
test: add synthetic knowledge retrieval benchmark
YangYuS8 Jul 16, 2026
328ac49
fix: flush knowledge evaluation results before exit
YangYuS8 Jul 16, 2026
17d78c9
build: include approved knowledge binaries
YangYuS8 Jul 16, 2026
2c9b3ef
docs: surface CozeLoop and VikingDB knowledge capabilities
YangYuS8 Jul 16, 2026
97a140e
ci: resolve VikingDB retriever dependency once
YangYuS8 Jul 16, 2026
8299cbd
build: add VikingDB knowledge dependencies
github-actions[bot] Jul 16, 2026
ef59e4d
ci: restore read-only VikingDB validation
YangYuS8 Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .env.knowledge.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
NIVORA_KNOWLEDGE_ADDR=127.0.0.1:3110
NIVORA_KNOWLEDGE_SHARED_SECRET=replace-with-a-long-random-secret
NIVORA_KNOWLEDGE_MIN_SCORE=0.75
NIVORA_KNOWLEDGE_OVERSAMPLE=3
NIVORA_KNOWLEDGE_REQUEST_TIMEOUT=15s

VIKINGDB_HOST=api-vikingdb.volces.com
VIKINGDB_REGION=cn-beijing
VIKINGDB_AK=your-access-key
VIKINGDB_SK=your-secret-key
VIKINGDB_SCHEME=https
VIKINGDB_COLLECTION=nivora_support_knowledge
VIKINGDB_INDEX=approved_hybrid
VIKINGDB_PARTITION=default
VIKINGDB_CONNECTION_TIMEOUT_SECONDS=5
VIKINGDB_WITH_MULTIMODAL=true
# Required only when VIKINGDB_WITH_MULTIMODAL=false.
VIKINGDB_EMBEDDING_MODEL=
VIKINGDB_USE_SPARSE=true
VIKINGDB_DENSE_WEIGHT=0.7
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
.PHONY: build test fmt vet run eval
.PHONY: build test fmt vet run eval knowledge knowledge-eval

build:
go build -trimpath -o bin/nivora ./cmd/nivora
go build -trimpath -o bin/nivora-eval ./cmd/nivora-eval
go build -trimpath -o bin/nivora-knowledge ./cmd/nivora-knowledge
go build -trimpath -o bin/nivora-knowledge-eval ./cmd/nivora-knowledge-eval

test:
go test ./...
Expand All @@ -18,3 +20,9 @@ run:

eval:
go run ./cmd/nivora-eval

knowledge:
go run ./cmd/nivora-knowledge

knowledge-eval:
go run ./cmd/nivora-knowledge-eval
31 changes: 19 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@ Lumio is the first planned provider integration, but Nivora itself does not know

- Eino `ChatModelAgent` with Volcengine Ark through the official `eino-ext` adapter
- ordered Ark endpoint failover before streaming begins
- optional CozeLoop tracing and PromptHub policy versions with strict trace redaction and bundled fallback
- capability- and scope-driven Tool registration
- provider-neutral Tools for knowledge, customer context, resources, diagnosis, transactions, and human-support cases
- Provider-side approved-knowledge reference service using the official Eino VikingDB retriever
- tenant, approval, freshness, provenance, and score validation after semantic retrieval
- black-box customer-support and knowledge-retrieval JSONL evaluation tools
- bounded Provider retries for idempotent reads and idempotent support-case creation
- stable Server-Sent Events protocol with heartbeat comments
- private service authentication between the product BFF and Nivora
- real Provider readiness checks with short caching
- global concurrency and queue protection
- Prometheus-compatible runtime metrics
- black-box JSONL regression evaluation for answers, Tool use, latency, and refusal behavior
- loopback-first production deployment example
- loopback-first production deployment examples

## Architecture

Expand All @@ -30,10 +33,12 @@ Browser
-> Product BFF (session, tenant, brand, scopes, rate limit)
-> Nivora :3100 (Eino runtime, private)
-> Product Provider API (authorization and business truth)
-> Product services and database / approved knowledge service
-> Product services and database
-> approved knowledge service :3110
-> VikingDB
```

Nivora does not accept a Provider URL from chat requests and does not connect to a product database. The configured Provider remains the source of truth.
Nivora does not accept a Provider URL from chat requests and does not connect to a product database or VikingDB. The configured Provider remains the source of truth.

## Run locally

Expand Down Expand Up @@ -93,9 +98,9 @@ Tool results are not forwarded to the browser. They remain inside the Agent run.

## Security boundary

- Bind Nivora to `127.0.0.1` or a private VPC address.
- Do not expose port `3100` through a public reverse proxy.
- Use separate secrets for product-to-Nivora and Nivora-to-Provider authentication.
- Bind Nivora and its reference services to loopback or private VPC addresses.
- Do not expose ports `3100` or `3110` through a public reverse proxy.
- Use separate secrets for product-to-Nivora, Nivora-to-Provider, and Provider-to-knowledge authentication.
- The Provider API must enforce customer ownership and redact internal fields.
- Anonymous requests can receive only explicitly granted knowledge and case scopes.
- Nivora currently performs read operations plus idempotent `case.create` only.
Expand All @@ -104,6 +109,8 @@ Tool results are not forwarded to the browser. They remain inside the Agent run.

- [Runtime API v1](docs/runtime-api.md)
- [Provider API v1](docs/provider-api.md)
- [CozeLoop integration](docs/cozeloop.md)
- [Approved VikingDB knowledge](docs/approved-knowledge.md)
- [Customer-support evaluation](docs/evaluation.md)
- [Volcengine production stack](docs/volcengine-production-stack.md)

Expand All @@ -115,12 +122,12 @@ make test
make vet
make build
make eval
make knowledge
make knowledge-eval
```

## Roadmap

1. Integrate CozeLoop tracing, prompt versioning, token accounting, and evaluator scores with strict redaction.
2. Build a Provider-backed knowledge pipeline that can use VikingDB for approved semantic retrieval.
3. Add durable conversations, audit logs, and support cases in Nivora's own storage.
4. Add shadow and canary modes for safe production rollout.
5. Add Eino interrupt/resume for human approval of future high-risk actions.
1. Add durable conversations, audit logs, and support cases in Nivora's own storage.
2. Add the production security, load, and shadow-traffic acceptance suite.
3. Add Eino interrupt/resume for human approval of future high-risk actions.
125 changes: 125 additions & 0 deletions cmd/nivora-knowledge-eval/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package main

import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"

"github.com/Nesoriel/nivora/internal/knowledgeeval"
"github.com/Nesoriel/nivora/pkg/knowledge"
)

func main() {
datasetPath := flag.String("dataset", env("NIVORA_KNOWLEDGE_EVAL_DATASET", "evals/knowledge.example.jsonl"), "JSONL evaluation dataset")
baseURL := flag.String("url", env("NIVORA_KNOWLEDGE_EVAL_URL", "http://127.0.0.1:3110"), "approved knowledge service URL")
secret := flag.String("key", strings.TrimSpace(os.Getenv("NIVORA_KNOWLEDGE_SHARED_SECRET")), "knowledge service key")
outputPath := flag.String("output", "", "optional JSONL output path")
requestTimeout := flag.Duration("timeout", 20*time.Second, "timeout for each retrieval")
flag.Parse()

if strings.TrimSpace(*secret) == "" {
fatalf("knowledge service key is required")
}
file, err := os.Open(*datasetPath)
if err != nil {
fatalf("open dataset: %v", err)
}
defer file.Close()
cases, err := knowledgeeval.LoadJSONL(file)
if err != nil {
fatalf("load dataset: %v", err)
}

writer, closeWriter, err := resultWriter(*outputPath)
if err != nil {
fatalf("create result writer: %v", err)
}
encoder := json.NewEncoder(writer)
client := &http.Client{Timeout: *requestTimeout}
failed := 0
for _, item := range cases {
observation := runCase(context.Background(), client, strings.TrimRight(*baseURL, "/"), *secret, item)
result := knowledgeeval.Evaluate(item, observation)
if !result.Passed {
failed++
}
if err := encoder.Encode(result); err != nil {
_ = closeWriter()
fatalf("write result: %v", err)
}
}
if err := closeWriter(); err != nil {
fatalf("close result writer: %v", err)
}
if failed > 0 {
fmt.Fprintf(os.Stderr, "%d of %d knowledge evaluation cases failed\n", failed, len(cases))
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "all %d knowledge evaluation cases passed\n", len(cases))
}

func runCase(parent context.Context, client *http.Client, baseURL, secret string, item knowledgeeval.Case) knowledgeeval.Observation {
payload, err := json.Marshal(map[string]any{
"tenant_id": item.TenantID,
"query": item.Query,
"limit": 10,
})
if err != nil {
return knowledgeeval.Observation{Error: err.Error()}
}
request, err := http.NewRequestWithContext(parent, http.MethodPost, baseURL+"/v1/search", bytes.NewReader(payload))
if err != nil {
return knowledgeeval.Observation{Error: err.Error()}
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("X-Nivora-Knowledge-Key", secret)
started := time.Now()
response, err := client.Do(request)
duration := time.Since(started)
if err != nil {
return knowledgeeval.Observation{Duration: duration, Error: err.Error()}
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(response.Body, 8*1024))
return knowledgeeval.Observation{Duration: duration, Error: fmt.Sprintf("status %d: %s", response.StatusCode, strings.TrimSpace(string(body)))}
}
var body struct {
Items []knowledge.Item `json:"items"`
}
decoder := json.NewDecoder(io.LimitReader(response.Body, 2<<20))
if err := decoder.Decode(&body); err != nil {
return knowledgeeval.Observation{Duration: duration, Error: err.Error()}
}
return knowledgeeval.Observation{Items: body.Items, Duration: duration}
}

func resultWriter(path string) (io.Writer, func() error, error) {
if strings.TrimSpace(path) == "" {
return os.Stdout, func() error { return nil }, nil
}
file, err := os.Create(path)
if err != nil {
return nil, func() error { return nil }, err
}
return file, file.Close, nil
}

func env(name, fallback string) string {
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
return value
}
return fallback
}

func fatalf(format string, values ...any) {
fmt.Fprintf(os.Stderr, format+"\n", values...)
os.Exit(2)
}
Loading
Loading