Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 27 additions & 0 deletions features/virtual-knowledge-base/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Virtual Knowledge Base feature bundle environment variables
# Copy this file to .env and adjust values before running scripts

# Core toggle
VIRTUAL_KB_ENABLED=true

# Database configuration (can reuse main DB or dedicated schema)
VIRTUAL_KB_DB_HOST=localhost
VIRTUAL_KB_DB_PORT=5432
VIRTUAL_KB_DB_USER=weknora
VIRTUAL_KB_DB_PASSWORD=weknora
VIRTUAL_KB_DB_NAME=weknora
VIRTUAL_KB_DB_SSLMODE=disable

# Cache / Redis configuration (optional)
VIRTUAL_KB_REDIS_ADDR=localhost:6379
VIRTUAL_KB_REDIS_DB=3
VIRTUAL_KB_REDIS_PASSWORD=

# Application defaults
VIRTUAL_KB_MAX_TAGS_PER_DOCUMENT=50
VIRTUAL_KB_MAX_VIRTUAL_KBS_PER_USER=100
VIRTUAL_KB_SEARCH_CACHE_TTL=3600

# API security
VIRTUAL_KB_JWT_SECRET=change-me
VIRTUAL_KB_API_KEY=change-me
96 changes: 96 additions & 0 deletions features/virtual-knowledge-base/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Virtual Knowledge Base Feature Bundle

This bundle introduces a virtual knowledge base subsystem for WeKnora that enables user-defined document tagging, dynamic virtual knowledge base construction, and enhanced search experiences without modifying the existing WeKnora codebase.

> **Important:** All files under `features/virtual-knowledge-base/` are self-contained and are intended to be submitted as a standalone pull request. Integrators can review, merge, and selectively wire these components into the core application as needed.

## Contents

- Architecture and design documentation
- Backend Go modules (models, repositories, services, handlers)
- PostgreSQL migration scripts
- Vue 3 / Vite frontend examples (aligned with main project stack)
- API specifications and Postman collections
- End-to-end, integration, and unit test stubs
- Deployment scripts and Docker assets
- Examples and integration guides

## Design Overview

- **Purpose**: Extend WeKnora with a pluggable virtual knowledge base (VKB) subsystem that introduces tag-driven document organization without touching the core application.
- **Scope**: Provides Go services, PostgreSQL migrations, and a Vue 3 frontend bundle that can run standalone or be merged into the primary console when needed.
- **Integration Strategy**: Ship as an isolated feature pack (`features/virtual-knowledge-base/`) so reviewers can evaluate, cherry-pick, or iteratively integrate components.

## Feature Highlights

- **Tag Taxonomy Management**: Define tag categories, create tags with weights, and assign tags to documents via REST APIs exposed under `/api/v1/virtual-kb` (`internal/`, `web/src/api/tag.ts`).
- **Virtual Knowledge Base Builder**: Compose VKB instances using tag filters, boolean operators, and weights; manage instances through `VirtualKBManagement.vue` with `VirtualKBList.vue` + `VirtualKBEditor.vue`.
- **Enhanced Search Pipeline**: Execute searches scoped by VKB filters or ad-hoc tag conditions; frontend surface provided in `EnhancedSearch.vue`, backend hooks outlined in `internal/service/impl/virtual_kb_service.go`.
- **Extensible Frontend Shell**: Vue router factory (`web/src/router/index.ts`) and Pinia-ready structure enable easy embedding into the main console while keeping the demo runnable in isolation.

## Quick Start

```bash
# 1. Load environment variables
cp .env.example .env

# 2. Review feature configuration
cat config/virtual-kb.yaml

# 3. Run database migrations
./scripts/migrate.sh

# 4. Build backend and frontend assets
./scripts/build.sh

# 5. Execute tests (unit + integration stubs)
./scripts/test.sh

# 6. Launch the feature stack (standalone mode)
docker compose -f docker/docker-compose.virtual-kb.yml up --build
```

### Frontend (Vue) Quick Start

```bash
cd web

# Install dependencies (sub-project only, does not affect main project)
npm install

# Local development
npm run dev

# Run type checking
npm run type-check

# Build static assets (output directory: web/dist/)
npm run build

# Preview build artifacts
npm run preview
```

## Integration Overview

1. Review architectural notes in `DESIGN.md`.
2. Apply migrations located in `migrations/` to your PostgreSQL instance.
3. Register backend handlers via adapters (see backend integration examples).
4. Vue sub-project is located in `web/`. Run `npm install && npm run build` to generate `web/dist/`, which can be embedded into existing Vue applications as needed.
5. Follow deployment notes in this README or backend documentation.

### Routing & Store Integration Notes

- This sub-project provides `src/router/index.ts` as example route mapping for demo/preview purposes only. When migrating pages to the main project, you can directly import `TagManagement.vue`, `VirtualKBManagement.vue`, `EnhancedSearch.vue` and mount them to existing routes.
- For global state management, refer to the main project's Pinia structure. You can extend modules in `src/stores/` or reuse existing stores.
- HTTP requests are uniformly encapsulated in `src/api/`, defaulting to `/api/v1/virtual-kb`. You can adjust the baseURL or interceptors as needed during integration.

## Support

For questions related to this feature bundle, refer to:

- Backend API documentation in `internal/` modules
- Database schema in `migrations/`
- Frontend component examples in `web/src/`

This feature pack is designed for evaluation and iterative integration. Adjust and extend as required to fit your production environment.
17 changes: 17 additions & 0 deletions features/virtual-knowledge-base/config/virtual-kb.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
virtual_kb:
enabled: true
limits:
max_tags_per_document: 50
max_virtual_kbs_per_user: 100
max_filters_per_virtual_kb: 20
search:
cache_enabled: true
cache_ttl_seconds: 3600
default_vector_threshold: 0.25
default_keyword_threshold: 0.2
ui:
base_path: "/virtual-kb"
theme:
primary_color: "#1f7aec"
accent_color: "#ff9f1c"
danger_color: "#ff595e"
10 changes: 10 additions & 0 deletions features/virtual-knowledge-base/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/tencent/weknora/features/virtualkb

go 1.24

require (
github.com/gin-gonic/gin v1.10.0
github.com/json-iterator/go v1.1.12
gorm.io/driver/postgres v1.5.7
gorm.io/gorm v1.25.11
)
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package handler

import (
"net/http"
"strconv"

"github.com/gin-gonic/gin"
service "github.com/tencent/weknora/features/virtualkb/internal/service/interfaces"
"github.com/tencent/weknora/features/virtualkb/internal/types"
)

// DocumentTagHandler exposes endpoints for document-tag relationships.
type DocumentTagHandler struct {
service service.DocumentTagService
}

// NewDocumentTagHandler creates a new handler instance.
func NewDocumentTagHandler(service service.DocumentTagService) *DocumentTagHandler {
return &DocumentTagHandler{service: service}
}

// RegisterRoutes wires document-tag routes under the provided router group.
func (h *DocumentTagHandler) RegisterRoutes(rg *gin.RouterGroup) {
docs := rg.Group("/documents")
{
docs.GET(":id/tags", h.listTagsForDocument)
docs.POST(":id/tags", h.assignTag)
docs.PUT(":id/tags/:tag_id", h.updateTag)
docs.DELETE(":id/tags/:tag_id", h.removeTag)
}

tags := rg.Group("/tags")
{
tags.GET(":tag_id/documents", h.listDocumentsForTag)
}
}

func (h *DocumentTagHandler) assignTag(c *gin.Context) {
documentID := c.Param("id")
var req assignTagRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondBadRequest(c, "invalid tag assignment payload", err)
return
}

assignment := &types.DocumentTag{
DocumentID: documentID,
TagID: req.TagID,
Weight: req.Weight,
}
if err := h.service.AssignTag(c.Request.Context(), assignment); err != nil {
respondBadRequest(c, "failed to assign tag", err)
return
}
respondSuccess(c, http.StatusCreated, assignment)
}

func (h *DocumentTagHandler) updateTag(c *gin.Context) {
documentID := c.Param("id")
tagID, err := parseIDParam(c.Param("tag_id"))
if err != nil {
respondBadRequest(c, "invalid tag id", err)
return
}

var req assignTagRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondBadRequest(c, "invalid tag assignment payload", err)
return
}

assignment := &types.DocumentTag{
DocumentID: documentID,
TagID: tagID,
Weight: req.Weight,
}
if err := h.service.UpdateTag(c.Request.Context(), assignment); err != nil {
respondBadRequest(c, "failed to update tag assignment", err)
return
}
respondSuccess(c, http.StatusOK, assignment)
}

func (h *DocumentTagHandler) removeTag(c *gin.Context) {
documentID := c.Param("id")
tagID, err := parseIDParam(c.Param("tag_id"))
if err != nil {
respondBadRequest(c, "invalid tag id", err)
return
}

if err := h.service.RemoveTag(c.Request.Context(), documentID, tagID); err != nil {
respondInternal(c, err)
return
}
respondSuccess(c, http.StatusNoContent, nil)
}

func (h *DocumentTagHandler) listTagsForDocument(c *gin.Context) {
documentID := c.Param("id")
tags, err := h.service.ListTags(c.Request.Context(), documentID)
if err != nil {
respondInternal(c, err)
return
}
respondSuccess(c, http.StatusOK, tags)
}

func (h *DocumentTagHandler) listDocumentsForTag(c *gin.Context) {
tagID, err := parseIDParam(c.Param("tag_id"))
if err != nil {
respondBadRequest(c, "invalid tag id", err)
return
}
documents, err := h.service.ListDocuments(c.Request.Context(), tagID)
if err != nil {
respondInternal(c, err)
return
}
respondSuccess(c, http.StatusOK, documents)
}

type assignTagRequest struct {
TagID int64 `json:"tag_id" binding:"required"`
Weight *float64 `json:"weight"`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package handler

import (
"net/http"

"github.com/gin-gonic/gin"
service "github.com/tencent/weknora/features/virtualkb/internal/service/interfaces"
"github.com/tencent/weknora/features/virtualkb/internal/types"
)

// EnhancedSearchHandler provides HTTP endpoints for enhanced search.
type EnhancedSearchHandler struct {
service service.EnhancedSearchService
}

// NewEnhancedSearchHandler constructs a new handler.
func NewEnhancedSearchHandler(service service.EnhancedSearchService) *EnhancedSearchHandler {
return &EnhancedSearchHandler{service: service}
}

// RegisterRoutes wires enhanced search routes.
func (h *EnhancedSearchHandler) RegisterRoutes(rg *gin.RouterGroup) {
rg.POST("", h.search)
}

func (h *EnhancedSearchHandler) search(c *gin.Context) {
var req types.EnhancedSearchRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondBadRequest(c, "invalid search payload", err)
return
}

resp, err := h.service.Search(c.Request.Context(), &req)
if err != nil {
respondBadRequest(c, "failed to perform enhanced search", err)
return
}
respondSuccess(c, http.StatusOK, resp)
}
35 changes: 35 additions & 0 deletions features/virtual-knowledge-base/internal/handler/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package handler

import (
"net/http"

"github.com/gin-gonic/gin"
)

// respondSuccess writes a success JSON response.
func respondSuccess(c *gin.Context, status int, data any) {
if data == nil {
c.Status(status)
return
}
c.JSON(status, gin.H{"data": data})
}

// respondError writes an error JSON response.
func respondError(c *gin.Context, status int, message string, err error) {
payload := gin.H{"error": message}
if err != nil {
payload["details"] = err.Error()
}
c.JSON(status, payload)
}

// respondBadRequest writes a standardized bad request response.
func respondBadRequest(c *gin.Context, message string, err error) {
respondError(c, http.StatusBadRequest, message, err)
}

// respondInternal writes an internal server error response.
func respondInternal(c *gin.Context, err error) {
respondError(c, http.StatusInternalServerError, "internal server error", err)
}
Loading