Files
ragflow/internal/router/router.go
Jack 8f4809d1b5 feat: implement POST /api/v1/searchbots/retrieval_test (#15710)
## What problem does this PR solve?

Implements `POST /api/v1/searchbots/retrieval_test` in the Go API
server, aligning with the Python `bot_api.py` counterpart. Also applies
security hardening and consistency fixes discovered during CTO-level
code review:

- **Missing endpoint**: `retrieval_test` was not available in Go,
requiring Python fallback
- **Security**: Both `chunkHandler` and `searchBotHandler` leaked
`err.Error()` to API consumers
- **Python alignment**: Default values, empty question handling, and
`top_k <= 0` validation differed from Python behavior
- **Test gaps**: `chunkHandler.RetrievalTest` had zero unit tests;
several edge cases uncovered

## Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring

## Summary

### New Endpoint
- `POST /api/v1/searchbots/retrieval_test` — retrieval test with full
field support (page, size, top_k, use_kg, cross_languages, keyword,
similarity_threshold, vector_similarity_weight)

### New Type
- `common.StringSlice` — JSON type that accepts both `"kb1"` and
`["kb1", "kb2"]`, matching Python API flexibility

### Security
- Both `searchBotHandler` and `chunkHandler` now use `common.Warn()` +
generic error messages instead of leaking `err.Error()` to API consumers
- All error responses include consistent `"data": nil` shape
- `chunkHandler.RetrievalTest` uses interface-based DI (`chunkService`)
to enable testability

### Python Alignment
- Handler-level defaults align with Python `bot_api.py` (page=1,
size=30, top_k=1024, similarity_threshold=0.0,
vector_similarity_weight=0.3)
- `top_k <= 0` validation matching Python behavior
- Empty/whitespace question returns 200 + empty result (matches
`chunk_api.py`)
- `chunkHandler` `Datasets` field uses `common.StringSlice` for
string-or-array flexibility

### Refactoring
- `ChunkServiceIface` → `ChunkRetriever`, `chunkSvcIface` →
`chunkService` (Go-conventional naming)
- Extracted `applyRetrievalDefaults`, `toRetrievalServiceRequest` from
handler body
- Regex moved to package-level var in `parseRelatedQuestions`
- `service.RetrievalTestRequest.Datasets` type changed to
`common.StringSlice`
- `chunkHandler` now uses consumer-side interface for DI

### Tests
- 37 unit tests across both handlers: auth, validation, defaults,
StringSlice edge cases, empty/whitespace KbID, service errors, JSON
format, `top_k <= 0`, field mapping verification

## Files Changed

| File | Change |
|------|--------|
| `cmd/server_main.go` | Wire new handler + chunkService +
difyRetrievalHandler |
| `internal/common/json_types.go` | New StringSlice type |
| `internal/common/json_types_test.go` | StringSlice tests |
| `internal/handler/chunk.go` | Interface-based DI, security, Python
alignment, defaults |
| `internal/handler/chunk_test.go` | New — 9 comprehensive tests |
| `internal/handler/searchbot.go` | New endpoint + refactoring + `top_k
<= 0` validation |
| `internal/handler/searchbot_test.go` | 18 tests covering all edge
cases |
| `internal/router/router.go` | Register new route +
difyRetrievalHandler |
| `internal/service/chunk.go` | Datasets type → StringSlice, Question
binding relaxed |

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:16:56 +08:00

547 lines
22 KiB
Go

//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package router
import (
"github.com/gin-gonic/gin"
"ragflow/internal/handler"
)
type Router struct {
authHandler *handler.AuthHandler
userHandler *handler.UserHandler
tenantHandler *handler.TenantHandler
documentHandler *handler.DocumentHandler
datasetsHandler *handler.DatasetsHandler
systemHandler *handler.SystemHandler
knowledgebaseHandler *handler.KnowledgebaseHandler
chunkHandler *handler.ChunkHandler
llmHandler *handler.LLMHandler
chatHandler *handler.ChatHandler
chatSessionHandler *handler.ChatSessionHandler
connectorHandler *handler.ConnectorHandler
searchHandler *handler.SearchHandler
fileHandler *handler.FileHandler
memoryHandler *handler.MemoryHandler
mcpHandler *handler.MCPHandler
skillSearchHandler *handler.SkillSearchHandler
providerHandler *handler.ProviderHandler
agentHandler *handler.AgentHandler
searchBotHandler *handler.SearchBotHandler
difyRetrievalHandler *handler.DifyRetrievalHandler
pluginHandler *handler.PluginHandler
}
// NewRouter create router
func NewRouter(
authHandler *handler.AuthHandler,
userHandler *handler.UserHandler,
tenantHandler *handler.TenantHandler,
documentHandler *handler.DocumentHandler,
datasetsHandler *handler.DatasetsHandler,
systemHandler *handler.SystemHandler,
knowledgebaseHandler *handler.KnowledgebaseHandler,
chunkHandler *handler.ChunkHandler,
llmHandler *handler.LLMHandler,
chatHandler *handler.ChatHandler,
chatSessionHandler *handler.ChatSessionHandler,
connectorHandler *handler.ConnectorHandler,
searchHandler *handler.SearchHandler,
fileHandler *handler.FileHandler,
memoryHandler *handler.MemoryHandler,
mcpHandler *handler.MCPHandler,
skillSearchHandler *handler.SkillSearchHandler,
providerHandler *handler.ProviderHandler,
agentHandler *handler.AgentHandler,
searchBotHandler *handler.SearchBotHandler,
difyRetrievalHandler *handler.DifyRetrievalHandler,
pluginHandler *handler.PluginHandler,
) *Router {
return &Router{
authHandler: authHandler,
userHandler: userHandler,
tenantHandler: tenantHandler,
documentHandler: documentHandler,
datasetsHandler: datasetsHandler,
systemHandler: systemHandler,
knowledgebaseHandler: knowledgebaseHandler,
chunkHandler: chunkHandler,
llmHandler: llmHandler,
chatHandler: chatHandler,
chatSessionHandler: chatSessionHandler,
connectorHandler: connectorHandler,
searchHandler: searchHandler,
fileHandler: fileHandler,
memoryHandler: memoryHandler,
mcpHandler: mcpHandler,
skillSearchHandler: skillSearchHandler,
providerHandler: providerHandler,
agentHandler: agentHandler,
searchBotHandler: searchBotHandler,
difyRetrievalHandler: difyRetrievalHandler,
pluginHandler: pluginHandler,
}
}
// Setup setup routes
func (r *Router) Setup(engine *gin.Engine) {
// Mark all responses from Go with a header for debugging.
engine.Use(func(c *gin.Context) {
c.Header("X-API-Source", "go")
c.Next()
})
// Log all HTTP requests.
engine.Use(gin.Logger())
// Health check
engine.GET("/health", r.systemHandler.Health)
// System endpoints
engine.GET("/v1/system/configs", r.systemHandler.GetConfigs)
//engine.POST("/v1/user/register", r.userHandler.Register)
// User logout endpoint
engine.GET("/v1/user/logout", r.userHandler.Logout)
// OAuth callbacks are invoked by third-party providers and cannot rely on
// the RAGFlow auth middleware.
engine.GET("/connectors/gmail/oauth/web/callback", r.connectorHandler.GmailWebOAuthCallback)
engine.GET("/connectors/google-drive/oauth/web/callback", r.connectorHandler.GoogleDriveWebOAuthCallback)
apiNoAuth := engine.Group("/api/v1")
{
apiNoAuth.GET("/system/ping", r.systemHandler.Ping)
apiNoAuth.GET("/system/config", r.systemHandler.GetConfig)
apiNoAuth.GET("/system/version", r.systemHandler.GetVersion)
apiNoAuth.GET("/system/healthz", r.systemHandler.Healthz)
// User login channels endpoint
apiNoAuth.GET("/auth/login/channels", r.userHandler.GetLoginChannels)
// User login by email endpoint
apiNoAuth.POST("/auth/login", r.userHandler.LoginByEmail)
// OAuth / OIDC login routes. The static "channels" segment is
// registered before the wildcard, so gin's tree resolves
// /auth/login/channels to GetLoginChannels and other values to
// OAuthLogin without conflict.
apiNoAuth.GET("/auth/login/:channel", r.userHandler.OAuthLogin)
apiNoAuth.GET("/auth/oauth/:channel/callback", r.userHandler.OAuthCallback)
// Register
apiNoAuth.POST("/users", r.userHandler.Register)
// Document images are embedded directly in pages and match Python's public route.
apiNoAuth.GET("/documents/images/:image_id", r.documentHandler.GetDocumentImage)
// Google redirects here after Gmail / Google Drive web OAuth completes.
apiNoAuth.GET("/connectors/gmail/oauth/web/callback", r.connectorHandler.GmailWebOAuthCallback)
apiNoAuth.GET("/connectors/google-drive/oauth/web/callback", r.connectorHandler.GoogleDriveWebOAuthCallback)
}
// Protected routes
authorized := engine.Group("")
authorized.Use(r.authHandler.AuthMiddleware())
{
// User info endpoint
authorized.GET("/v1/user/info", r.userHandler.Info)
// User tenant info endpoint
authorized.GET("/v1/user/tenant_info", r.tenantHandler.TenantInfo)
// Tenant list endpoint
authorized.GET("/v1/tenant/list", r.tenantHandler.TenantList)
// User settings endpoint
authorized.POST("/v1/user/setting", r.userHandler.Setting)
// User change password endpoint
authorized.POST("/v1/user/setting/password", r.userHandler.ChangePassword)
// User set tenant info endpoint
authorized.POST("/v1/user/set_tenant_info", r.userHandler.SetTenantInfo)
// API v1 route group
v1 := authorized.Group("/api/v1")
{
// Auth routes
auth := v1.Group("/auth")
{
// User logout endpoint
auth.POST("/logout", r.userHandler.Logout)
}
// Users routes
users := v1.Group("/users")
{
users.GET("/me", r.userHandler.Info)
// User settings endpoint
users.PATCH("/me", r.userHandler.Setting)
// User tenant info endpoint
users.GET("/me/models", r.tenantHandler.TenantInfo)
// User set tenant info endpoint
users.PATCH("/me/models", r.userHandler.SetTenantInfo)
}
tenants := v1.Group("/tenants")
{
tenants.GET("", r.tenantHandler.TenantList)
tenants.PATCH("/:tenant_id", r.tenantHandler.AcceptTenantInvite)
tenants.GET("/:tenant_id/users", r.tenantHandler.ListTenantMembers)
tenants.POST("/:tenant_id/users", r.tenantHandler.AddTenantMember)
tenants.DELETE("/:tenant_id/users", r.tenantHandler.RemoveTenantMember)
}
v1.GET("/tenant/list", r.tenantHandler.TenantList)
// Document routes
documents := v1.Group("/documents")
{
documents.POST("", r.documentHandler.CreateDocument)
documents.GET("", r.documentHandler.ListDocuments)
documents.GET("/artifact/:filename", r.documentHandler.GetDocumentArtifact)
documents.GET("/:id/preview", r.documentHandler.GetDocumentPreview)
documents.GET("/:id", r.documentHandler.GetDocumentByID)
documents.PUT("/:id", r.documentHandler.UpdateDocument)
documents.DELETE("/:id", r.documentHandler.DeleteDocument)
}
// Chat routes
chats := v1.Group("/chats")
{
chats.GET("", r.chatHandler.ListChats)
chats.GET("/:chat_id", r.chatHandler.GetChat)
chats.GET("/:chat_id/sessions", r.chatSessionHandler.ListChatSessions)
}
// Searchbot routes
v1.POST("/searchbots/related_questions", r.searchBotHandler.Handle)
v1.POST("/searchbots/retrieval_test", r.searchBotHandler.RetrievalTest)
// Dataset routes
datasets := v1.Group("/datasets")
{
datasets.GET("", r.datasetsHandler.ListDatasets)
datasets.GET("/:dataset_id", r.datasetsHandler.GetDataset)
datasets.GET("/:dataset_id/graph", r.datasetsHandler.GetKnowledgeGraph)
datasets.DELETE("/:dataset_id/tags", r.datasetsHandler.RemoveTags)
datasets.DELETE("/:dataset_id/graph", r.datasetsHandler.DeleteKnowledgeGraph)
datasets.POST("", r.datasetsHandler.CreateDataset)
datasets.DELETE("", r.datasetsHandler.DeleteDatasets)
datasets.POST("/search", r.datasetsHandler.SearchDatasets)
datasets.GET("/metadata/flattened", r.datasetsHandler.ListMetadataFlattened)
// Dataset ingestion logs
datasets.GET("/:dataset_id/ingestions/summary", r.datasetsHandler.GetIngestionSummary)
datasets.GET("/:dataset_id/ingestions", r.datasetsHandler.ListIngestionLogs)
datasets.GET("/:dataset_id/ingestions/:log_id", r.datasetsHandler.GetIngestionLog)
// Metadata Config
datasets.GET("/:dataset_id/metadata/config", r.datasetsHandler.GetMetadataConfig)
datasets.PUT("/:dataset_id/metadata/config", r.datasetsHandler.UpdateMetadataConfig)
// Dataset documents
datasets.GET("/:dataset_id/documents", r.documentHandler.ListDocuments)
datasets.GET("/:dataset_id/documents/:document_id", r.documentHandler.DownloadDocument)
datasets.DELETE("/:dataset_id/documents", r.documentHandler.DeleteDocuments)
// Dataset document chunk
datasets.GET("/:dataset_id/documents/:document_id/chunks/:chunk_id", r.chunkHandler.Get)
datasets.POST("/:dataset_id/documents/parse", r.documentHandler.ParseDocuments)
datasets.POST("/:dataset_id/documents/stop", r.documentHandler.StopParseDocuments)
datasets.DELETE("/:dataset_id/documents/:document_id/chunks", r.chunkHandler.RemoveChunks)
}
// Search routes
searches := v1.Group("/searches")
{
searches.GET("", r.searchHandler.ListSearches)
searches.POST("", r.searchHandler.CreateSearch)
searches.GET("/:search_id", r.searchHandler.GetSearch)
searches.PUT("/:search_id", r.searchHandler.UpdateSearch)
searches.DELETE("/:search_id", r.searchHandler.DeleteSearch)
}
file := v1.Group("/files")
{
file.POST("", r.fileHandler.UploadFile)
file.GET("", r.fileHandler.ListFiles)
file.DELETE("", r.fileHandler.DeleteFiles)
file.POST("/move", r.fileHandler.MoveFiles)
file.GET("/:id/ancestors", r.fileHandler.GetFileAncestors)
file.GET("/:id/parent", r.fileHandler.GetParentFolder)
file.GET("/:id", r.fileHandler.Download)
}
// Author routes
authors := v1.Group("/authors")
{
authors.GET("/:author_id/documents", r.documentHandler.GetDocumentsByAuthorID)
}
// Memory routes
memory := v1.Group("/memories")
{
memory.POST("", r.memoryHandler.CreateMemory)
memory.PUT("/:memory_id", r.memoryHandler.UpdateMemory)
memory.DELETE("/:memory_id", r.memoryHandler.DeleteMemory)
memory.GET("", r.memoryHandler.ListMemories)
memory.GET("/:memory_id/config", r.memoryHandler.GetMemoryConfig)
memory.GET("/:memory_id", r.memoryHandler.GetMemoryMessages)
}
// TODO: Message routes - Implementation pending - depends on CanvasService, TaskService and embedding engine
// message := v1.Group("/messages")
// {
// message.POST("", r.memoryHandler.AddMessage)
// message.DELETE("/:memory_id/:message_id", r.memoryHandler.ForgetMessage)
// message.PUT("/:memory_id/:message_id", r.memoryHandler.UpdateMessage)
// message.GET("/search", r.memoryHandler.SearchMessage)
// message.GET("", r.memoryHandler.GetMessages)
// message.GET("/:memory_id/:message_id/content", r.memoryHandler.GetMessageContent)
// }
// Skill search routes
skills := v1.Group("/skills")
{
// Skill Space management
skills.GET("/spaces", r.skillSearchHandler.ListSpaces)
skills.POST("/spaces", r.skillSearchHandler.CreateSpace)
skills.GET("/spaces/:space_id", r.skillSearchHandler.GetSpace)
skills.PUT("/spaces/:space_id", r.skillSearchHandler.UpdateSpace)
skills.DELETE("/spaces/:space_id", r.skillSearchHandler.DeleteSpace)
skills.GET("/space/by-folder", r.skillSearchHandler.GetSpaceByFolder)
// Skill search config
skills.GET("/config", r.skillSearchHandler.GetConfig)
skills.POST("/config", r.skillSearchHandler.UpdateConfig)
// Skill search and indexing
skills.POST("/search", r.skillSearchHandler.Search)
skills.POST("/index", r.skillSearchHandler.IndexSkills)
skills.DELETE("/index", r.skillSearchHandler.DeleteSkillIndex)
skills.POST("/reindex", r.skillSearchHandler.Reindex)
}
// provider pool route group
provider := v1.Group("/providers")
{
provider.GET("/", r.providerHandler.ListProviders)
provider.PUT("/", r.providerHandler.AddProvider)
provider.GET("/:provider_name", r.providerHandler.ShowProvider)
provider.DELETE("/:provider_name", r.providerHandler.DeleteProvider)
provider.GET("/:provider_name/models", r.providerHandler.ListModels)
provider.GET("/:provider_name/models/:model_name", r.providerHandler.ShowModel)
provider.POST("/:provider_name/instances", r.providerHandler.CreateProviderInstance)
provider.GET("/:provider_name/instances", r.providerHandler.ListProviderInstances)
provider.GET("/:provider_name/instances/:instance_name", r.providerHandler.ShowProviderInstance)
provider.GET("/:provider_name/instances/:instance_name/balance", r.providerHandler.ShowInstanceBalance)
provider.GET("/:provider_name/instances/:instance_name/connection", r.providerHandler.CheckInstanceConnection)
provider.GET("/:provider_name/connection", r.providerHandler.CheckConnection)
provider.GET("/:provider_name/instances/:instance_name/tasks", r.providerHandler.ListTasks)
provider.GET("/:provider_name/instances/:instance_name/tasks/:task_id", r.providerHandler.ShowTask)
provider.PUT("/:provider_name/instances/:instance_name", r.providerHandler.AlterProviderInstance)
provider.DELETE("/:provider_name/instances", r.providerHandler.DropProviderInstance)
provider.GET("/:provider_name/instances/:instance_name/models", r.providerHandler.ListInstanceModels)
provider.PATCH("/:provider_name/instances/:instance_name/models/*model_name", r.providerHandler.EnableOrDisableModel)
provider.POST("/:provider_name/instances/:instance_name/models", r.providerHandler.AddModel)
provider.DELETE("/:provider_name/instances/:instance_name/models", r.providerHandler.DropInstanceModels)
v1.POST("/chat/completions", r.providerHandler.ChatToModel)
v1.POST("/embeddings", r.providerHandler.EmbedText)
v1.POST("/rerank", r.providerHandler.RerankDocument)
v1.POST("/audio/transcriptions", r.providerHandler.TranscribeAudio)
v1.POST("/audio/speech", r.providerHandler.AudioSpeech)
v1.POST("/file/ocr", r.providerHandler.OCRFile)
v1.POST("/file/parse", r.providerHandler.ParseFile)
}
model := v1.Group("/models")
{
model.GET("/", r.tenantHandler.GetModels)
model.PATCH("/", r.tenantHandler.SetModels)
}
// Agent routes
agents := v1.Group("/agents")
{
agents.GET("", r.agentHandler.ListAgents)
agents.GET("/templates", r.agentHandler.ListTemplates)
agents.GET("/:agent_id/versions", r.agentHandler.ListAgentVersions)
agents.GET("/:agent_id/versions/:version_id", r.agentHandler.GetAgentVersion)
agents.POST("/:agent_id/upload", r.agentHandler.UploadAgentFile)
agents.PUT("/:agent_id/tags", r.agentHandler.UpdateAgentTags)
}
// Plugin routes
plugin := v1.Group("/plugin")
{
plugin.GET("/tools", r.pluginHandler.ListLLMTools)
}
connector := v1.Group("/connectors")
{
connector.GET("/", r.connectorHandler.ListConnectors)
connector.POST("/", r.connectorHandler.CreateConnector)
connector.POST("/google/oauth/web/start", r.connectorHandler.StartGoogleWebOAuth)
connector.POST("/google/oauth/web/result", r.connectorHandler.PollGoogleWebOAuthResult)
connector.GET("/:connector_id", r.connectorHandler.GetConnector)
connector.GET("/:connector_id/logs", r.connectorHandler.ListLogs)
connector.DELETE("/:connector_id", r.connectorHandler.DeleteConnector)
connector.POST("/:connector_id/rebuild", r.connectorHandler.RebuildConnector)
connector.POST("/:connector_id/test", r.connectorHandler.TestConnector)
}
// MCP server routes. Per-server CRUD ships via separate PRs that
// share the same handler/service: GET list (#15253), GET by id
// (#15254), POST create (#15260, merged), PUT (#15261), DELETE
// (#15262, merged). This PR adds only the non-overlapping
// endpoints: import and test.
mcp := v1.Group("/mcp")
{
mcp.POST("/servers", r.mcpHandler.CreateMCPServer)
mcp.GET("/servers", r.mcpHandler.ListMCPServers)
mcp.PUT("/servers/:mcp_id", r.mcpHandler.UpdateMCPServer)
mcp.DELETE("/servers/:mcp_id", r.mcpHandler.DeleteMCPServer)
mcp.POST("/servers/import", r.mcpHandler.ImportMCPServers)
mcp.POST("/servers/:mcp_id/test", r.mcpHandler.TestMCPServer)
}
system := v1.Group("/system")
{
system.GET("/configs", r.systemHandler.GetConfigs)
system.GET("/status", r.systemHandler.GetStatus)
system.GET("/stats", r.systemHandler.GetStats)
config := system.Group("/config")
{
config.GET("/log", r.systemHandler.GetLogLevel)
config.PUT("/log", r.systemHandler.SetLogLevel)
}
//log := system.Group("/log")
//{
// // /api/v1/system/log GET
// log.GET("", r.systemHandler.GetLogLevel)
// // /api/v1/system/log PUT
// log.PUT("", r.systemHandler.SetLogLevel)
//}
tokens := system.Group("/tokens")
{
// list tokens /api/v1/system/tokens GET
tokens.GET("", r.systemHandler.ListTokens)
// create token /api/v1/system/tokens POST
tokens.POST("", r.systemHandler.CreateToken)
// delete token /api/v1/system/tokens/:token DELETE
tokens.DELETE("/:token", r.systemHandler.DeleteToken)
}
}
}
// Knowledge base routes
kb := v1.Group("/kb")
{
kb.POST("/update", r.knowledgebaseHandler.UpdateKB)
kb.POST("/update_metadata_setting", r.knowledgebaseHandler.UpdateMetadataSetting)
kb.GET("/detail", r.knowledgebaseHandler.GetDetail)
kb.GET("/tags", r.knowledgebaseHandler.ListTagsFromKbs)
kb.GET("/get_meta", r.knowledgebaseHandler.GetMeta)
kb.GET("/basic_info", r.knowledgebaseHandler.GetBasicInfo)
// KB ID specific routes
kbByID := kb.Group("/:kb_id")
{
kbByID.GET("/tags", r.knowledgebaseHandler.ListTags)
kbByID.POST("/rename_tag", r.knowledgebaseHandler.RenameTag)
kbByID.GET("/knowledge_graph", r.knowledgebaseHandler.KnowledgeGraph)
kbByID.DELETE("/knowledge_graph", r.knowledgebaseHandler.DeleteKnowledgeGraph)
}
}
// Tenant routes (per-tenant resources)
tenant := v1.Group("/tenant")
{
tenant.POST("/chunk_store", r.tenantHandler.CreateChunkStore) // Internal API only for GO
tenant.DELETE("/chunk_store", r.tenantHandler.DeleteChunkStore) // Internal API only for GO
tenant.POST("/metadata_store", r.tenantHandler.CreateMetadataStore) // Internal API only for GO
tenant.DELETE("/metadata_store", r.tenantHandler.DeleteMetadataStore) // Internal API only for GO
tenant.POST("/insert_chunks_from_file", r.tenantHandler.InsertChunksFromFile) // Internal API only for GO
tenant.POST("/insert_metadata_from_file", r.tenantHandler.InsertMetadataFromFile) // Internal API only for GO
}
// Document routes
doc := v1.Group("/document")
{
doc.POST("/list", r.documentHandler.ListDocuments)
doc.POST("/metadata/summary", r.documentHandler.MetadataSummary)
doc.POST("/set_meta", r.documentHandler.SetMeta)
doc.POST("/delete_meta", r.documentHandler.DeleteMeta) // Internal API only for GO
}
v1.GET("/thumbnails", r.documentHandler.GetThumbnail)
// Chunk routes
chunk := v1.Group("/chunk")
{
chunk.POST("/list", r.chunkHandler.List)
chunk.POST("/update", r.chunkHandler.UpdateChunk) // Internal API only for GO
}
// Chat routes
chat := authorized.Group("/v1/dialog")
{
chat.POST("/next", r.chatHandler.ListChatsNext)
chat.POST("/set", r.chatHandler.SetDialog)
chat.POST("/rm", r.chatHandler.RemoveChats)
}
// Chat session (conversation) routes
session := authorized.Group("/v1/conversation")
{
session.POST("/set", r.chatSessionHandler.SetChatSession)
session.POST("/rm", r.chatSessionHandler.RemoveChatSessions)
session.GET("/list", r.chatSessionHandler.ListChatSessions)
session.POST("/completion", r.chatSessionHandler.Completion)
}
// Connector routes
connector := authorized.Group("/v1/connector")
{
connector.GET("/list", r.connectorHandler.ListConnectors)
connector.GET("/:connector_id", r.connectorHandler.GetConnector)
connector.POST("/:connector_id/rebuild", r.connectorHandler.RebuildConnector)
}
// File routes
file := authorized.Group("/v1/file")
{
file.GET("/root_folder", r.fileHandler.GetRootFolder)
file.GET("/parent_folder", r.fileHandler.GetParentFolder)
file.GET("/all_parent_folder", r.fileHandler.GetAllParentFolders)
}
}
// Dify retrieval routes
dify := authorized.Group("/api/v1/dify")
{
dify.POST("/retrieval", r.difyRetrievalHandler.Retrieval)
dify.GET("/retrieval", r.difyRetrievalHandler.Retrieval)
}
apiNoAuth.GET("/dify/retrieval/health", r.difyRetrievalHandler.HealthCheck)
// Handle undefined routes
engine.NoRoute(handler.HandleNoRoute)
}