Skip to content
Draft
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
1 change: 1 addition & 0 deletions client/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type AgentQARequest struct {
Query string `json:"query"` // Required query text
KnowledgeBaseIDs []string `json:"knowledge_base_ids,omitempty"` // Optional KBs for this query
KnowledgeIDs []string `json:"knowledge_ids,omitempty"` // Optional specific knowledge IDs for this query
FolderScopes []FolderScope `json:"folder_scopes,omitempty"` // Optional folder scopes paired with KB IDs
AgentEnabled bool `json:"agent_enabled"` // Whether to run in agent mode
AgentID string `json:"agent_id,omitempty"` // Optional custom agent ID
WebSearchEnabled bool `json:"web_search_enabled"` // Whether to enable web search
Expand Down
41 changes: 41 additions & 0 deletions client/folder_scope_json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package client

import (
"encoding/json"
"strings"
"testing"
)

func TestFolderScopeSDKJSONShapes(t *testing.T) {
kbID := "11111111-1111-1111-1111-111111111111"
folderID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
multi, err := json.Marshal(KnowledgeQARequest{
Query: "q", KnowledgeBaseIDs: []string{kbID},
FolderScopes: []FolderScope{{KnowledgeBaseID: kbID, FolderIDs: []string{folderID, "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"}}},
})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(multi), `"folder_ids":["`+folderID) || strings.Contains(string(multi), `"folder_id"`) {
t.Fatalf("unexpected multi-folder payload: %s", multi)
}

legacy, err := json.Marshal(SearchKnowledgeRequest{
Query: "q", KnowledgeBaseIDs: []string{kbID},
FolderScopes: []FolderScope{{KnowledgeBaseID: kbID, FolderID: &folderID}},
})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(legacy), `"folder_id":"`+folderID+`"`) {
t.Fatalf("legacy folder_id missing: %s", legacy)
}

omitted, err := json.Marshal(AgentQARequest{Query: "q"})
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(omitted), "folder_scopes") {
t.Fatalf("empty scopes must be omitted: %s", omitted)
}
}
64 changes: 51 additions & 13 deletions client/knowledge.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Knowledge struct {
ID string `json:"id"`
TenantID uint64 `json:"tenant_id"`
KnowledgeBaseID string `json:"knowledge_base_id"`
FolderID *string `json:"folder_id"`
TagID string `json:"tag_id"`
Type string `json:"type"`
Title string `json:"title"`
Expand Down Expand Up @@ -86,14 +87,14 @@ var ErrDuplicateURL = errors.New("URL already exists")
// KnowledgeProcessOverrides stores per-upload parse config overrides sent as process_config.
// When nil, the server uses the knowledge base defaults only.
type KnowledgeProcessOverrides struct {
ParserEngineRules []ParserEngineRule `json:"parser_engine_rules,omitempty"`
ChunkingConfig *ChunkingConfig `json:"chunking_config,omitempty"`
EnableMultimodel *bool `json:"enable_multimodel,omitempty"`
VLMConfig *VLMConfig `json:"vlm_config,omitempty"`
ASRConfig *ASRConfig `json:"asr_config,omitempty"`
QuestionGenerationConfig *QuestionGenerationConfig `json:"question_generation_config,omitempty"`
GraphEnabled *bool `json:"graph_enabled,omitempty"`
ExtractConfig *ExtractConfig `json:"extract_config,omitempty"`
ParserEngineRules []ParserEngineRule `json:"parser_engine_rules,omitempty"`
ChunkingConfig *ChunkingConfig `json:"chunking_config,omitempty"`
EnableMultimodel *bool `json:"enable_multimodel,omitempty"`
VLMConfig *VLMConfig `json:"vlm_config,omitempty"`
ASRConfig *ASRConfig `json:"asr_config,omitempty"`
QuestionGenerationConfig *QuestionGenerationConfig `json:"question_generation_config,omitempty"`
GraphEnabled *bool `json:"graph_enabled,omitempty"`
ExtractConfig *ExtractConfig `json:"extract_config,omitempty"`
}

// CreateKnowledgeFromFile creates a knowledge entry from a local file path
Expand All @@ -108,6 +109,33 @@ type KnowledgeProcessOverrides struct {
func (c *Client) CreateKnowledgeFromFile(ctx context.Context,
knowledgeBaseID string, filePath string, metadata map[string]string, enableMultimodel *bool, customFileName string, channel string,
processConfig *KnowledgeProcessOverrides,
) (*Knowledge, error) {
return c.CreateKnowledgeFromFileInFolder(
ctx,
knowledgeBaseID,
filePath,
metadata,
enableMultimodel,
customFileName,
channel,
processConfig,
nil,
)
}

// CreateKnowledgeFromFileInFolder creates a knowledge entry from a local file
// and optionally places it directly in folderID. A nil folderID preserves the
// legacy behavior and creates the knowledge in the knowledge-base root.
func (c *Client) CreateKnowledgeFromFileInFolder(
ctx context.Context,
knowledgeBaseID string,
filePath string,
metadata map[string]string,
enableMultimodel *bool,
customFileName string,
channel string,
processConfig *KnowledgeProcessOverrides,
folderID *string,
) (*Knowledge, error) {
// Open the local file
file, err := os.Open(filePath)
Expand Down Expand Up @@ -174,6 +202,12 @@ func (c *Client) CreateKnowledgeFromFile(ctx context.Context,
}
}

if folderID != nil {
if err := writer.WriteField("folder_id", *folderID); err != nil {
return nil, fmt.Errorf("failed to write folder_id field: %w", err)
}
}

if processConfig != nil {
processConfigBytes, err := json.Marshal(processConfig)
if err != nil {
Expand Down Expand Up @@ -236,6 +270,9 @@ type CreateKnowledgeFromURLRequest struct {
Channel string `json:"channel,omitempty"`
// ProcessConfig is optional per-upload parse config overrides (KnowledgeProcessOverrides).
ProcessConfig *KnowledgeProcessOverrides `json:"process_config,omitempty"`
// FolderID optionally places the new knowledge directly in a folder.
// Nil or omitted means the knowledge-base root.
FolderID *string `json:"folder_id,omitempty"`
}

// CreateKnowledgeFromURL creates a knowledge entry from a URL.
Expand Down Expand Up @@ -527,7 +564,7 @@ func (c *Client) ReparseKnowledge(ctx context.Context, knowledgeID string) (*Kno
// - pending — task has not started
// - processing — DocReader / chunking / embedding stage
// - finalizing — primary parse done, enrichment subtasks (summary,
// question generation, graph extract) still running
// question generation, graph extract) still running
//
// Returns an error when the knowledge is in a terminal state
// (completed, failed) or already being deleted.
Expand Down Expand Up @@ -693,10 +730,11 @@ func (c *Client) UpdateImageInfo(ctx context.Context,

// CreateManualKnowledgeRequest contains the parameters for creating a manual Markdown knowledge entry.
type CreateManualKnowledgeRequest struct {
Title string `json:"title"`
Content string `json:"content"`
TagID string `json:"tag_id,omitempty"`
Channel string `json:"channel,omitempty"`
Title string `json:"title"`
Content string `json:"content"`
TagID string `json:"tag_id,omitempty"`
Channel string `json:"channel,omitempty"`
FolderID *string `json:"folder_id,omitempty"`
}

// UpdateManualKnowledgeRequest contains the parameters for updating a manual Markdown knowledge entry.
Expand Down
46 changes: 36 additions & 10 deletions client/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,18 +207,43 @@ type ImageAttachment struct {
Caption string `json:"caption,omitempty"` // VLM analysis result
}

// FolderScope represents a union of folder subtree boundaries for one knowledge
// base. FolderID remains available for legacy callers; new callers should use
// FolderIDs and omit the entire scope for whole-KB retrieval.
type FolderScope struct {
KnowledgeBaseID string `json:"knowledge_base_id"`
FolderIDs []string `json:"folder_ids,omitempty"`
FolderID *string `json:"folder_id,omitempty"`
}

func (s FolderScope) MarshalJSON() ([]byte, error) {
if len(s.FolderIDs) > 0 {
return json.Marshal(struct {
KnowledgeBaseID string `json:"knowledge_base_id"`
FolderIDs []string `json:"folder_ids"`
FolderID *string `json:"folder_id,omitempty"`
}{s.KnowledgeBaseID, s.FolderIDs, s.FolderID})
}
// Preserve the old SDK's ability to send explicit folder_id:null.
return json.Marshal(struct {
KnowledgeBaseID string `json:"knowledge_base_id"`
FolderID *string `json:"folder_id"`
}{s.KnowledgeBaseID, s.FolderID})
}

// KnowledgeQARequest knowledge Q&A request
type KnowledgeQARequest struct {
Query string `json:"query"` // Query text for knowledge base search
KnowledgeBaseIDs []string `json:"knowledge_base_ids"` // Selected knowledge base IDs for this request
KnowledgeIDs []string `json:"knowledge_ids"` // Selected knowledge IDs for this request
AgentEnabled bool `json:"agent_enabled"` // Whether agent mode is enabled for this request
AgentID string `json:"agent_id"` // Selected custom agent ID for this request
WebSearchEnabled bool `json:"web_search_enabled"` // Whether web search is enabled for this request
SummaryModelID string `json:"summary_model_id"` // Optional summary model ID (overrides session default)
DisableTitle bool `json:"disable_title"` // Whether to disable auto title generation
Images []ImageAttachment `json:"images,omitempty"` // Attached images for multimodal chat
Channel string `json:"channel,omitempty"` // Source channel: "web", "api", "im", etc.
Query string `json:"query"` // Query text for knowledge base search
KnowledgeBaseIDs []string `json:"knowledge_base_ids"` // Selected knowledge base IDs for this request
KnowledgeIDs []string `json:"knowledge_ids"` // Selected knowledge IDs for this request
FolderScopes []FolderScope `json:"folder_scopes,omitempty"` // Folder scopes paired with KB IDs
AgentEnabled bool `json:"agent_enabled"` // Whether agent mode is enabled for this request
AgentID string `json:"agent_id"` // Selected custom agent ID for this request
WebSearchEnabled bool `json:"web_search_enabled"` // Whether web search is enabled for this request
SummaryModelID string `json:"summary_model_id"` // Optional summary model ID (overrides session default)
DisableTitle bool `json:"disable_title"` // Whether to disable auto title generation
Images []ImageAttachment `json:"images,omitempty"` // Attached images for multimodal chat
Channel string `json:"channel,omitempty"` // Source channel: "web", "api", "im", etc.
}

// LLMToolCall represents a function/tool call from the LLM
Expand Down Expand Up @@ -453,6 +478,7 @@ type SearchKnowledgeRequest struct {
KnowledgeBaseID string `json:"knowledge_base_id,omitempty"` // Single knowledge base ID (for backward compatibility)
KnowledgeBaseIDs []string `json:"knowledge_base_ids,omitempty"` // Knowledge base IDs (multi-KB support)
KnowledgeIDs []string `json:"knowledge_ids,omitempty"` // Specific knowledge (file) IDs
FolderScopes []FolderScope `json:"folder_scopes,omitempty"` // Folder scopes paired with KB IDs
TagIDs []string `json:"tag_ids,omitempty"` // Tag IDs for filtering within a single KB
MentionedItems []MentionedItem `json:"mentioned_items,omitempty"` // Optional scoped tag mentions
}
Expand Down
84 changes: 84 additions & 0 deletions frontend/src/api/chat/streamBody.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
export interface StreamFolderScope {
knowledge_base_id: string
folder_ids: string[]
}

export interface StartStreamParams {
session_id: any
query: any
knowledge_base_ids?: string[]
knowledge_ids?: string[]
folder_scopes?: StreamFolderScope[]
tag_ids?: string[]
agent_enabled?: boolean
agent_id?: string
agent_source_tenant_id?: string | number
web_search_enabled?: boolean
summary_model_id?: string
mcp_service_ids?: string[]
skill_names?: string[]
mentioned_items?: Array<{id: string; name: string; type: string; kb_type?: string; kb_id?: string; kb_name?: string; service_id?: string; skill_name?: string}>
images?: Array<{data: string}>
attachment_uploads?: Array<{data: string; file_name: string; file_size: number}>
attachment_ids?: string[]
suggestion_attribution?: { suggestion_set_id: string; question_id: string }
method: string
url: string
embed_token?: string
embed_session_sig?: string
embed_visitor_id?: string
}

export function buildStreamPostBody(params: StartStreamParams): Record<string, any> {
const postBody: Record<string, any> = {
query: params.query,
agent_enabled: params.agent_enabled !== undefined ? params.agent_enabled : true,
}
if (params.knowledge_base_ids !== undefined && params.knowledge_base_ids.length > 0) {
postBody.knowledge_base_ids = params.knowledge_base_ids
}
if (params.knowledge_ids !== undefined && params.knowledge_ids.length > 0) {
postBody.knowledge_ids = params.knowledge_ids
}
if (params.folder_scopes !== undefined && params.folder_scopes.length > 0) {
postBody.folder_scopes = params.folder_scopes
}
if (params.agent_id) {
postBody.agent_id = params.agent_id
}
if (params.agent_source_tenant_id) {
postBody.agent_source_tenant_id = Number(params.agent_source_tenant_id)
}
if (params.web_search_enabled !== undefined) {
postBody.web_search_enabled = params.web_search_enabled
}
if (params.summary_model_id) {
postBody.summary_model_id = params.summary_model_id
}
if (params.mcp_service_ids !== undefined && params.mcp_service_ids.length > 0) {
postBody.mcp_service_ids = params.mcp_service_ids
}
if (params.skill_names !== undefined && params.skill_names.length > 0) {
postBody.skill_names = params.skill_names
}
if (params.tag_ids !== undefined && params.tag_ids.length > 0) {
postBody.tag_ids = params.tag_ids
}
if (params.mentioned_items !== undefined && params.mentioned_items.length > 0) {
postBody.mentioned_items = params.mentioned_items
}
if (params.images !== undefined && params.images.length > 0) {
postBody.images = params.images
}
if (params.attachment_uploads !== undefined && params.attachment_uploads.length > 0) {
postBody.attachment_uploads = params.attachment_uploads
}
if (params.attachment_ids !== undefined && params.attachment_ids.length > 0) {
postBody.attachment_ids = params.attachment_ids
}
if (params.suggestion_attribution) {
postBody.suggestion_attribution = params.suggestion_attribution
}
postBody.channel = params.embed_token ? 'embed' : 'web'
return postBody
}
Loading
Loading