Refactor Agent Response Handling to Use Structured Response Types
- Updated the agent's Stream and response processing methods to return and handle *context.Response directly, eliminating the need for type assertions. - Simplified test cases by removing unnecessary type conversions and directly accessing response fields. - Enhanced the extraction of data from Next hook responses, ensuring more robust handling of custom data structures. - Improved overall code readability and maintainability by streamlining response handling logic across various components.
This commit is contained in:
parent
01bf7dce58
commit
637a0c9cbd
17 changed files with 243 additions and 335 deletions
|
|
@ -16,7 +16,7 @@ import (
|
||||||
|
|
||||||
// Stream stream the agent
|
// Stream stream the agent
|
||||||
// handler is optional, if not provided, a default handler will be used
|
// handler is optional, if not provided, a default handler will be used
|
||||||
func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Message, options ...*context.Options) (interface{}, error) {
|
func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Message, options ...*context.Options) (*context.Response, error) {
|
||||||
|
|
||||||
// Update logger with assistant ID and start logging
|
// Update logger with assistant ID and start logging
|
||||||
ctx.Logger.SetAssistantID(ast.ID)
|
ctx.Logger.SetAssistantID(ast.ID)
|
||||||
|
|
@ -376,7 +376,7 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa
|
||||||
// ================================================
|
// ================================================
|
||||||
// Execute Next Hook and Process Response
|
// Execute Next Hook and Process Response
|
||||||
// ================================================
|
// ================================================
|
||||||
var finalResponse interface{}
|
var finalResponse *context.Response
|
||||||
var nextResponse *context.NextHookResponse = nil
|
var nextResponse *context.NextHookResponse = nil
|
||||||
|
|
||||||
if ast.HookScript != nil {
|
if ast.HookScript != nil {
|
||||||
|
|
|
||||||
|
|
@ -56,19 +56,18 @@ func TestAgentNextStandard(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, response)
|
assert.NotNil(t, response)
|
||||||
|
|
||||||
resp := response.(*context.Response)
|
assert.NotNil(t, response.Completion)
|
||||||
assert.NotNil(t, resp.Completion)
|
assert.Nil(t, response.Next)
|
||||||
assert.Nil(t, resp.Next)
|
|
||||||
|
|
||||||
// Verify response structure
|
// Verify response structure
|
||||||
assert.Equal(t, "tests.realworld-next", resp.AssistantID)
|
assert.Equal(t, "tests.realworld-next", response.AssistantID)
|
||||||
assert.NotEmpty(t, resp.ContextID)
|
assert.NotEmpty(t, response.ContextID)
|
||||||
assert.NotEmpty(t, resp.RequestID)
|
assert.NotEmpty(t, response.RequestID)
|
||||||
assert.NotEmpty(t, resp.TraceID)
|
assert.NotEmpty(t, response.TraceID)
|
||||||
assert.NotEmpty(t, resp.ChatID)
|
assert.NotEmpty(t, response.ChatID)
|
||||||
|
|
||||||
// Verify completion has content
|
// Verify completion has content
|
||||||
assert.NotNil(t, resp.Completion.Content)
|
assert.NotNil(t, response.Completion.Content)
|
||||||
|
|
||||||
t.Log("✓ Standard response test passed")
|
t.Log("✓ Standard response test passed")
|
||||||
}
|
}
|
||||||
|
|
@ -94,19 +93,18 @@ func TestAgentNextCustomData(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, response)
|
assert.NotNil(t, response)
|
||||||
|
|
||||||
resp := response.(*context.Response)
|
assert.NotNil(t, response.Completion)
|
||||||
assert.NotNil(t, resp.Completion)
|
assert.NotNil(t, response.Next)
|
||||||
assert.NotNil(t, resp.Next)
|
|
||||||
|
|
||||||
// Verify response structure
|
// Verify response structure
|
||||||
assert.Equal(t, "tests.realworld-next", resp.AssistantID)
|
assert.Equal(t, "tests.realworld-next", response.AssistantID)
|
||||||
assert.NotEmpty(t, resp.ContextID)
|
assert.NotEmpty(t, response.ContextID)
|
||||||
assert.NotEmpty(t, resp.RequestID)
|
assert.NotEmpty(t, response.RequestID)
|
||||||
assert.NotEmpty(t, resp.TraceID)
|
assert.NotEmpty(t, response.TraceID)
|
||||||
|
|
||||||
// Verify custom data structure (from scenarioCustomData)
|
// Verify custom data structure (from scenarioCustomData)
|
||||||
// resp.Next contains the "data" field value from NextHookResponse
|
// response.Next contains the "data" field value from NextHookResponse
|
||||||
nextData, ok := resp.Next.(map[string]interface{})
|
nextData, ok := response.Next.(map[string]interface{})
|
||||||
assert.True(t, ok, "Next should be a map")
|
assert.True(t, ok, "Next should be a map")
|
||||||
assert.Equal(t, "custom_response", nextData["type"])
|
assert.Equal(t, "custom_response", nextData["type"])
|
||||||
assert.Equal(t, "This is a custom response from Next Hook", nextData["message"])
|
assert.Equal(t, "This is a custom response from Next Hook", nextData["message"])
|
||||||
|
|
@ -137,21 +135,19 @@ func TestAgentNextDelegate(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, response)
|
assert.NotNil(t, response)
|
||||||
|
|
||||||
resp := response.(*context.Response)
|
|
||||||
|
|
||||||
// Verify response structure
|
// Verify response structure
|
||||||
assert.NotEmpty(t, resp.AssistantID)
|
assert.NotEmpty(t, response.AssistantID)
|
||||||
assert.NotEmpty(t, resp.ContextID)
|
assert.NotEmpty(t, response.ContextID)
|
||||||
assert.NotEmpty(t, resp.RequestID)
|
assert.NotEmpty(t, response.RequestID)
|
||||||
assert.NotEmpty(t, resp.TraceID)
|
assert.NotEmpty(t, response.TraceID)
|
||||||
|
|
||||||
// Verify completion (delegated agent should have returned completion)
|
// Verify completion (delegated agent should have returned completion)
|
||||||
assert.NotNil(t, resp.Completion)
|
assert.NotNil(t, response.Completion)
|
||||||
assert.NotNil(t, resp.Completion.Content)
|
assert.NotNil(t, response.Completion.Content)
|
||||||
|
|
||||||
// Next should be from the delegated agent
|
// Next should be from the delegated agent
|
||||||
// If delegated agent also has Next hook, it will be present
|
// If delegated agent also has Next hook, it will be present
|
||||||
t.Logf("✓ Delegation test passed (delegated to: %s)", resp.AssistantID)
|
t.Logf("✓ Delegation test passed (delegated to: %s)", response.AssistantID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAgentNextConditional tests agent with conditional logic in Next Hook
|
// TestAgentNextConditional tests agent with conditional logic in Next Hook
|
||||||
|
|
@ -177,18 +173,17 @@ func TestAgentNextConditional(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, response)
|
assert.NotNil(t, response)
|
||||||
|
|
||||||
resp := response.(*context.Response)
|
assert.NotNil(t, response.Next)
|
||||||
assert.NotNil(t, resp.Next)
|
|
||||||
|
|
||||||
// Verify response structure
|
// Verify response structure
|
||||||
assert.Equal(t, "tests.realworld-next", resp.AssistantID)
|
assert.Equal(t, "tests.realworld-next", response.AssistantID)
|
||||||
assert.NotEmpty(t, resp.ContextID)
|
assert.NotEmpty(t, response.ContextID)
|
||||||
assert.NotEmpty(t, resp.RequestID)
|
assert.NotEmpty(t, response.RequestID)
|
||||||
assert.NotEmpty(t, resp.TraceID)
|
assert.NotEmpty(t, response.TraceID)
|
||||||
|
|
||||||
// Verify conditional response structure (from scenarioConditional)
|
// Verify conditional response structure (from scenarioConditional)
|
||||||
// resp.Next contains the "data" field value from NextHookResponse
|
// response.Next contains the "data" field value from NextHookResponse
|
||||||
nextData, ok := resp.Next.(map[string]interface{})
|
nextData, ok := response.Next.(map[string]interface{})
|
||||||
assert.True(t, ok, "Next should be a map")
|
assert.True(t, ok, "Next should be a map")
|
||||||
assert.Equal(t, "Conditional analysis complete", nextData["message"])
|
assert.Equal(t, "Conditional analysis complete", nextData["message"])
|
||||||
assert.Contains(t, nextData, "action")
|
assert.Contains(t, nextData, "action")
|
||||||
|
|
@ -224,19 +219,18 @@ func TestAgentWithoutNextHook(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, response)
|
assert.NotNil(t, response)
|
||||||
|
|
||||||
resp := response.(*context.Response)
|
assert.Nil(t, response.Next)
|
||||||
assert.Nil(t, resp.Next)
|
|
||||||
|
|
||||||
// Verify response structure
|
// Verify response structure
|
||||||
assert.Equal(t, "tests.create", resp.AssistantID)
|
assert.Equal(t, "tests.create", response.AssistantID)
|
||||||
assert.NotEmpty(t, resp.ContextID)
|
assert.NotEmpty(t, response.ContextID)
|
||||||
assert.NotEmpty(t, resp.RequestID)
|
assert.NotEmpty(t, response.RequestID)
|
||||||
assert.NotEmpty(t, resp.TraceID)
|
assert.NotEmpty(t, response.TraceID)
|
||||||
assert.NotEmpty(t, resp.ChatID)
|
assert.NotEmpty(t, response.ChatID)
|
||||||
|
|
||||||
// Verify completion
|
// Verify completion
|
||||||
assert.NotNil(t, resp.Completion)
|
assert.NotNil(t, response.Completion)
|
||||||
assert.NotNil(t, resp.Completion.Content)
|
assert.NotNil(t, response.Completion.Content)
|
||||||
|
|
||||||
t.Log("✓ No Next Hook test passed")
|
t.Log("✓ No Next Hook test passed")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ type agentCallerWrapper struct {
|
||||||
ast *Assistant
|
ast *Assistant
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *agentCallerWrapper) Stream(ctx *agentContext.Context, messages []agentContext.Message, options ...*agentContext.Options) (interface{}, error) {
|
func (w *agentCallerWrapper) Stream(ctx *agentContext.Context, messages []agentContext.Message, options ...*agentContext.Options) (*agentContext.Response, error) {
|
||||||
return w.ast.Stream(ctx, messages, options...)
|
return w.ast.Stream(ctx, messages, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// processNextResponse processes the Next hook's response and handles agent delegation or custom data
|
// processNextResponse processes the Next hook's response and handles agent delegation or custom data
|
||||||
func (ast *Assistant) processNextResponse(npc *NextProcessContext) (interface{}, error) {
|
func (ast *Assistant) processNextResponse(npc *NextProcessContext) (*agentContext.Response, error) {
|
||||||
// If no Next hook response, return standard response
|
// If no Next hook response, return standard response
|
||||||
if npc.NextResponse == nil {
|
if npc.NextResponse == nil {
|
||||||
return ast.buildStandardResponse(npc), nil
|
return ast.buildStandardResponse(npc), nil
|
||||||
|
|
@ -42,7 +42,7 @@ func (ast *Assistant) handleDelegation(
|
||||||
ctx *agentContext.Context,
|
ctx *agentContext.Context,
|
||||||
delegate *agentContext.DelegateConfig,
|
delegate *agentContext.DelegateConfig,
|
||||||
streamHandler func(message.StreamChunkType, []byte) int,
|
streamHandler func(message.StreamChunkType, []byte) int,
|
||||||
) (interface{}, error) {
|
) (*agentContext.Response, error) {
|
||||||
// Load the target assistant
|
// Load the target assistant
|
||||||
targetAssistant, err := Get(delegate.AgentID)
|
targetAssistant, err := Get(delegate.AgentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -62,7 +62,7 @@ func (ast *Assistant) handleDelegation(
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildStandardResponse builds the standard agent response when no custom Next hook processing is needed
|
// buildStandardResponse builds the standard agent response when no custom Next hook processing is needed
|
||||||
func (ast *Assistant) buildStandardResponse(npc *NextProcessContext) interface{} {
|
func (ast *Assistant) buildStandardResponse(npc *NextProcessContext) *agentContext.Response {
|
||||||
return &agentContext.Response{
|
return &agentContext.Response{
|
||||||
ContextID: npc.Context.ID,
|
ContextID: npc.Context.ID,
|
||||||
RequestID: npc.Context.RequestID(),
|
RequestID: npc.Context.RequestID(),
|
||||||
|
|
|
||||||
|
|
@ -98,39 +98,37 @@ func (ast *Assistant) checkSearchIntent(ctx *context.Context, messages []context
|
||||||
|
|
||||||
// Parse the result
|
// Parse the result
|
||||||
// Next hook returns {data: {need_search: bool, search_types: [], confidence: float}}
|
// Next hook returns {data: {need_search: bool, search_types: [], confidence: float}}
|
||||||
if response, ok := result.(*context.Response); ok {
|
// First try to get from Next hook response
|
||||||
// First try to get from Next hook response
|
if result.Next != nil {
|
||||||
if response.Next != nil {
|
if nextData, ok := result.Next.(map[string]interface{}); ok {
|
||||||
if nextData, ok := response.Next.(map[string]interface{}); ok {
|
// Check for data field (from Next hook's {data: result})
|
||||||
// Check for data field (from Next hook's {data: result})
|
var intentData map[string]interface{}
|
||||||
var intentData map[string]interface{}
|
if data, ok := nextData["data"].(map[string]interface{}); ok {
|
||||||
if data, ok := nextData["data"].(map[string]interface{}); ok {
|
intentData = data
|
||||||
intentData = data
|
} else {
|
||||||
} else {
|
intentData = nextData
|
||||||
intentData = nextData
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if needSearch, ok := intentData["need_search"].(bool); ok {
|
if needSearch, ok := intentData["need_search"].(bool); ok {
|
||||||
reason, _ := intentData["reason"].(string)
|
reason, _ := intentData["reason"].(string)
|
||||||
ctx.Logger.Debug("Search intent (from Next): need_search=%v, reason=%s", needSearch, reason)
|
ctx.Logger.Debug("Search intent (from Next): need_search=%v, reason=%s", needSearch, reason)
|
||||||
ast.sendIntentDone(ctx, loadingID, needSearch, reason)
|
ast.sendIntentDone(ctx, loadingID, needSearch, reason)
|
||||||
return needSearch
|
return needSearch
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback: parse from Completion.Content if Next hook didn't process
|
// Fallback: parse from Completion.Content if Next hook didn't process
|
||||||
if response.Completion != nil {
|
if result.Completion != nil {
|
||||||
content, ok := response.Completion.Content.(string)
|
content, ok := result.Completion.Content.(string)
|
||||||
if !ok || content == "" {
|
if !ok || content == "" {
|
||||||
ast.sendIntentDone(ctx, loadingID, true, "")
|
ast.sendIntentDone(ctx, loadingID, true, "")
|
||||||
return true
|
return true
|
||||||
}
|
|
||||||
needSearch, reason := parseNeedSearchFromContent(content)
|
|
||||||
ctx.Logger.Debug("Search intent (from Content): need_search=%v, reason=%s", needSearch, reason)
|
|
||||||
ast.sendIntentDone(ctx, loadingID, needSearch, reason)
|
|
||||||
return needSearch
|
|
||||||
}
|
}
|
||||||
|
needSearch, reason := parseNeedSearchFromContent(content)
|
||||||
|
ctx.Logger.Debug("Search intent (from Content): need_search=%v, reason=%s", needSearch, reason)
|
||||||
|
ast.sendIntentDone(ctx, loadingID, needSearch, reason)
|
||||||
|
return needSearch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default: proceed with search if we can't parse the result
|
// Default: proceed with search if we can't parse the result
|
||||||
|
|
|
||||||
|
|
@ -77,8 +77,7 @@ func TestSearchAutoDisabled(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, response)
|
require.NotNil(t, response)
|
||||||
|
|
||||||
resp := response.(*context.Response)
|
assert.NotNil(t, response.Completion, "should have completion")
|
||||||
assert.NotNil(t, resp.Completion, "should have completion")
|
|
||||||
t.Logf("✓ Stream executed without search (disabled)")
|
t.Logf("✓ Stream executed without search (disabled)")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,8 +118,7 @@ func TestSearchAutoFull(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
require.NotNil(t, response)
|
require.NotNil(t, response)
|
||||||
resp := response.(*context.Response)
|
assert.NotNil(t, response.Completion, "should have completion")
|
||||||
assert.NotNil(t, resp.Completion, "should have completion")
|
|
||||||
t.Logf("✓ Stream executed with full search config (Web + KB + DB)")
|
t.Logf("✓ Stream executed with full search config (Web + KB + DB)")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -101,8 +101,7 @@ func TestSearchAutoHookDisable(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, response)
|
require.NotNil(t, response)
|
||||||
|
|
||||||
resp := response.(*context.Response)
|
assert.NotNil(t, response.Completion, "should have completion")
|
||||||
assert.NotNil(t, resp.Completion, "should have completion")
|
|
||||||
t.Logf("✓ Stream executed with hook disabling search")
|
t.Logf("✓ Stream executed with hook disabling search")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,8 +87,7 @@ func TestSearchAutoKeyword(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
require.NotNil(t, response)
|
require.NotNil(t, response)
|
||||||
resp := response.(*context.Response)
|
assert.NotNil(t, response.Completion, "should have completion")
|
||||||
assert.NotNil(t, resp.Completion, "should have completion")
|
|
||||||
t.Logf("✓ Stream with keyword extraction executed successfully")
|
t.Logf("✓ Stream with keyword extraction executed successfully")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -127,8 +126,7 @@ func TestSearchAutoKeyword(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
require.NotNil(t, response)
|
require.NotNil(t, response)
|
||||||
resp := response.(*context.Response)
|
assert.NotNil(t, response.Completion, "should have completion")
|
||||||
assert.NotNil(t, resp.Completion, "should have completion")
|
|
||||||
t.Logf("✓ Stream with Skip.Keyword executed successfully")
|
t.Logf("✓ Stream with Skip.Keyword executed successfully")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -179,8 +177,7 @@ func TestSearchAutoKeywordNotConfigured(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
require.NotNil(t, response)
|
require.NotNil(t, response)
|
||||||
resp := response.(*context.Response)
|
assert.NotNil(t, response.Completion, "should have completion")
|
||||||
assert.NotNil(t, resp.Completion, "should have completion")
|
|
||||||
t.Logf("✓ Stream without keyword config executed successfully")
|
t.Logf("✓ Stream without keyword config executed successfully")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,8 +96,7 @@ func TestSearchAutoWeb(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
require.NotNil(t, response)
|
require.NotNil(t, response)
|
||||||
resp := response.(*context.Response)
|
assert.NotNil(t, response.Completion, "should have completion")
|
||||||
assert.NotNil(t, resp.Completion, "should have completion")
|
|
||||||
t.Logf("✓ Stream executed successfully with auto search")
|
t.Logf("✓ Stream executed successfully with auto search")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
// AgentCaller interface for calling agents (to avoid circular dependency)
|
// AgentCaller interface for calling agents (to avoid circular dependency)
|
||||||
// Used by content handlers (vision, audio, etc.) and search handlers (agent mode)
|
// Used by content handlers (vision, audio, etc.) and search handlers (agent mode)
|
||||||
type AgentCaller interface {
|
type AgentCaller interface {
|
||||||
Stream(ctx *agentContext.Context, messages []agentContext.Message, options ...*agentContext.Options) (interface{}, error)
|
Stream(ctx *agentContext.Context, messages []agentContext.Message, options ...*agentContext.Options) (*agentContext.Response, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AgentGetterFunc is a function type that gets an agent by ID
|
// AgentGetterFunc is a function type that gets an agent by ID
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,10 @@ func CallAgent(ctx *agentContext.Context, agentID string, message agentContext.M
|
||||||
|
|
||||||
// Extract text from agent response
|
// Extract text from agent response
|
||||||
// Two formats are supported:
|
// Two formats are supported:
|
||||||
// 1. Custom Hook response (from Next hook)
|
// 1. Custom Hook response (from Next hook) - response.Next
|
||||||
// 2. Standard Agent Stream response (LLM completion)
|
// 2. Standard Agent Stream response (LLM completion) - response.Completion
|
||||||
|
|
||||||
return extractTextFromAgentResponse(response)
|
return extractTextFromResponse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CallAgentWithFileInfo calls an agent to process content with file metadata
|
// CallAgentWithFileInfo calls an agent to process content with file metadata
|
||||||
|
|
@ -120,116 +120,63 @@ func CallAgentWithFileInfo(ctx *agentContext.Context, agentID string, message ag
|
||||||
return CallAgent(ctx, agentID, message)
|
return CallAgent(ctx, agentID, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractTextFromAgentResponse extracts text from agent response
|
// extractTextFromResponse extracts text from agent response
|
||||||
// Handles two main response formats from agent.Stream():
|
// Now that agent.Stream() returns *agentContext.Response directly,
|
||||||
//
|
// we can access fields without type assertions or JSON conversion.
|
||||||
// 1. Standard Response (No Next Hook or Next Hook returns nil):
|
|
||||||
// Structure: { completion: { content: "text" | [...ContentPart] } }
|
|
||||||
// Action: Extract text from completion.content field
|
|
||||||
//
|
|
||||||
// 2. Next Hook Response with Custom Data:
|
|
||||||
// Structure: { next: <any data from Next hook> }
|
|
||||||
// Action:
|
|
||||||
// - If next is string → return directly
|
|
||||||
// - If next is map/object → JSON stringify and return
|
|
||||||
// - This preserves the complete custom data structure from the hook
|
|
||||||
//
|
//
|
||||||
// Priority:
|
// Priority:
|
||||||
// 1. Check for "next" field (custom hook data) → return complete data
|
// 1. Check response.Next (custom hook data) → return complete data
|
||||||
// 2. Check for "completion" field (standard LLM response) → extract text only
|
// 2. Check response.Completion (standard LLM response) → extract text only
|
||||||
// 3. Fallback to direct string or JSON stringify
|
func extractTextFromResponse(response *agentContext.Response) (string, error) {
|
||||||
func extractTextFromAgentResponse(response interface{}) (string, error) {
|
|
||||||
if response == nil {
|
if response == nil {
|
||||||
return "", fmt.Errorf("agent returned nil response")
|
return "", fmt.Errorf("agent returned nil response")
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, try to convert to map if it's a struct
|
// Priority 1: Check Next field (custom hook data)
|
||||||
// agent.Stream() may return *agentContext.Response which needs to be converted
|
// If Next hook returns custom data, return the complete structure
|
||||||
var responseMap map[string]interface{}
|
if response.Next != nil {
|
||||||
|
|
||||||
// Check if it's already a map
|
|
||||||
if rm, ok := response.(map[string]interface{}); ok {
|
|
||||||
responseMap = rm
|
|
||||||
} else {
|
|
||||||
// Try to marshal and unmarshal to convert struct to map
|
|
||||||
jsonBytes, err := jsoniter.Marshal(response)
|
|
||||||
if err != nil {
|
|
||||||
// If it's a plain string, return directly
|
|
||||||
if responseStr, ok := response.(string); ok {
|
|
||||||
return responseStr, nil
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("failed to serialize agent response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal to map
|
|
||||||
if err := jsoniter.Unmarshal(jsonBytes, &responseMap); err != nil {
|
|
||||||
// If unmarshal fails, return the JSON string
|
|
||||||
return string(jsonBytes), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 1: Check for "next" field (custom hook data)
|
|
||||||
// If Next hook returns custom data, it's stored in the "next" field
|
|
||||||
// Return the complete custom data structure (preserve hook's intent)
|
|
||||||
if next, hasNext := responseMap["next"]; hasNext && next != nil {
|
|
||||||
// If next is a string, return directly
|
// If next is a string, return directly
|
||||||
if nextStr, ok := next.(string); ok {
|
if nextStr, ok := response.Next.(string); ok {
|
||||||
return nextStr, nil
|
return nextStr, nil
|
||||||
}
|
}
|
||||||
// Otherwise, JSON stringify to preserve complete structure
|
// Otherwise, JSON stringify to preserve complete structure
|
||||||
jsonBytes, err := jsoniter.Marshal(next)
|
jsonBytes, err := jsoniter.Marshal(response.Next)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to serialize next hook data: %w", err)
|
return "", fmt.Errorf("failed to serialize next hook data: %w", err)
|
||||||
}
|
}
|
||||||
return string(jsonBytes), nil
|
return string(jsonBytes), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 2: Check for "completion" field (standard LLM response)
|
// Priority 2: Check Completion field (standard LLM response)
|
||||||
// Extract text content from the LLM completion
|
// Extract text content from the LLM completion
|
||||||
if completion, hasCompletion := responseMap["completion"]; hasCompletion && completion != nil {
|
if response.Completion != nil {
|
||||||
if completionMap, ok := completion.(map[string]interface{}); ok {
|
// Content can be string or []ContentPart (multimodal)
|
||||||
// Extract content from completion
|
switch v := response.Completion.Content.(type) {
|
||||||
if content, hasContent := completionMap["content"]; hasContent {
|
case string:
|
||||||
// Content can be string or []ContentPart (multimodal)
|
// Simple text content
|
||||||
switch v := content.(type) {
|
return v, nil
|
||||||
case string:
|
case []interface{}:
|
||||||
// Simple text content
|
// Multimodal content array - extract all text parts
|
||||||
return v, nil
|
var text string
|
||||||
case []interface{}:
|
for _, part := range v {
|
||||||
// Multimodal content array - extract all text parts
|
if partMap, ok := part.(map[string]interface{}); ok {
|
||||||
var text string
|
if partType, _ := partMap["type"].(string); partType == "text" {
|
||||||
for _, part := range v {
|
if textContent, ok := partMap["text"].(string); ok {
|
||||||
if partMap, ok := part.(map[string]interface{}); ok {
|
text += textContent
|
||||||
if partType, _ := partMap["type"].(string); partType == "text" {
|
|
||||||
if textContent, ok := partMap["text"].(string); ok {
|
|
||||||
text += textContent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if text != "" {
|
|
||||||
return text, nil
|
|
||||||
}
|
|
||||||
// No text found in content parts
|
|
||||||
return "", fmt.Errorf("no text content found in completion content parts")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if text != "" {
|
||||||
|
return text, nil
|
||||||
|
}
|
||||||
|
// No text found in content parts
|
||||||
|
return "", fmt.Errorf("no text content found in completion content parts")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: Try to find a "content" field directly (shouldn't happen normally)
|
// No content found
|
||||||
if content, hasContent := responseMap["content"]; hasContent {
|
return "", fmt.Errorf("no content found in agent response")
|
||||||
if contentStr, ok := content.(string); ok {
|
|
||||||
return contentStr, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last resort: JSON stringify the entire response
|
|
||||||
jsonBytes, err := jsoniter.Marshal(response)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to serialize agent response: %w", err)
|
|
||||||
}
|
|
||||||
return string(jsonBytes), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CallMCPTool calls an MCP tool to process content
|
// CallMCPTool calls an MCP tool to process content
|
||||||
|
|
|
||||||
|
|
@ -150,46 +150,20 @@ func (p *AgentProvider) Search(ctx *agentContext.Context, req *types.Request) (*
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseAgentResponse parses the agent response into search result items
|
// parseAgentResponse parses the agent's *context.Response into search result items
|
||||||
// The agent should return a JSON structure with search results
|
// Now that agent.Stream() returns *context.Response directly,
|
||||||
func (p *AgentProvider) parseAgentResponse(response interface{}, source types.SourceType) ([]*types.ResultItem, int, string) {
|
// we can access fields without type assertions.
|
||||||
if response == nil {
|
//
|
||||||
|
// The agent returns search results in response.Next field
|
||||||
|
func (p *AgentProvider) parseAgentResponse(response *agentContext.Response, source types.SourceType) ([]*types.ResultItem, int, string) {
|
||||||
|
if response == nil || response.Next == nil {
|
||||||
return nil, 0, "Agent returned nil response"
|
return nil, 0, "Agent returned nil response"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to extract data from response
|
// Extract data from Next field
|
||||||
var data map[string]interface{}
|
data := extractNextData(response.Next)
|
||||||
|
if data == nil {
|
||||||
// Handle different response types
|
return nil, 0, "Failed to extract data from agent response"
|
||||||
switch v := response.(type) {
|
|
||||||
case map[string]interface{}:
|
|
||||||
data = v
|
|
||||||
case string:
|
|
||||||
// Try to parse as JSON
|
|
||||||
if err := json.Unmarshal([]byte(v), &data); err != nil {
|
|
||||||
return nil, 0, fmt.Sprintf("Failed to parse agent response as JSON: %v", err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// Try to marshal and unmarshal
|
|
||||||
jsonBytes, err := json.Marshal(response)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, fmt.Sprintf("Failed to serialize agent response: %v", err)
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(jsonBytes, &data); err != nil {
|
|
||||||
return nil, 0, fmt.Sprintf("Failed to parse agent response: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for "next" field (custom hook data)
|
|
||||||
if next, hasNext := data["next"]; hasNext && next != nil {
|
|
||||||
if nextMap, ok := next.(map[string]interface{}); ok {
|
|
||||||
data = nextMap
|
|
||||||
} else if nextStr, ok := next.(string); ok {
|
|
||||||
// Try to parse as JSON
|
|
||||||
if err := json.Unmarshal([]byte(nextStr), &data); err != nil {
|
|
||||||
return nil, 0, fmt.Sprintf("Failed to parse next hook data: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract items from data
|
// Extract items from data
|
||||||
|
|
@ -230,3 +204,34 @@ func (p *AgentProvider) parseAgentResponse(response interface{}, source types.So
|
||||||
|
|
||||||
return items, total, ""
|
return items, total, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractNextData extracts the actual data from response.Next field
|
||||||
|
// Handles nested structures like { "data": { ... } }
|
||||||
|
func extractNextData(next interface{}) map[string]interface{} {
|
||||||
|
if next == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := next.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
// Check for "data" wrapper
|
||||||
|
if data, ok := v["data"].(map[string]interface{}); ok {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
case string:
|
||||||
|
// Try to parse as JSON
|
||||||
|
var data map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(v), &data); err == nil {
|
||||||
|
return extractNextData(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Try to handle other types by converting to JSON and back
|
||||||
|
if bytes, err := json.Marshal(next); err == nil {
|
||||||
|
var data map[string]interface{}
|
||||||
|
if err := json.Unmarshal(bytes, &data); err == nil {
|
||||||
|
return extractNextData(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,30 +64,38 @@ func (p *AgentProvider) Extract(ctx *agentContext.Context, content string, opts
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := agent.Stream(ctx, messages, options)
|
response, err := agent.Stream(ctx, messages, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("agent call failed: %w", err)
|
return nil, fmt.Errorf("agent call failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug: log the result type and value
|
// Parse the result from response.Next
|
||||||
// fmt.Printf("DEBUG Agent result type: %T, value: %+v\n", result, result)
|
return p.parseResponse(response)
|
||||||
|
|
||||||
// Parse the result
|
|
||||||
return p.parseResult(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseResult extracts keywords from the agent's response
|
// parseResponse extracts keywords from the agent's *context.Response
|
||||||
// The agent should return data in NextHookResponse format: { data: { keywords: [...] } }
|
// Now that agent.Stream() returns *context.Response directly,
|
||||||
// The Stream() response wraps this in: { next: { data: { keywords: [...] } } }
|
// we can access fields without type assertions.
|
||||||
func (p *AgentProvider) parseResult(result interface{}) ([]string, error) {
|
//
|
||||||
if result == nil {
|
// The agent returns keywords in response.Next field
|
||||||
|
func (p *AgentProvider) parseResponse(response *agentContext.Response) ([]string, error) {
|
||||||
|
if response == nil || response.Next == nil {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.parseNextData(response.Next)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseNextData extracts keywords from Next hook data
|
||||||
|
func (p *AgentProvider) parseNextData(next interface{}) ([]string, error) {
|
||||||
|
if next == nil {
|
||||||
return []string{}, nil
|
return []string{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to convert to map first (most common case)
|
// Try to convert to map first (most common case)
|
||||||
var data map[string]interface{}
|
var data map[string]interface{}
|
||||||
|
|
||||||
switch v := result.(type) {
|
switch v := next.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
data = v
|
data = v
|
||||||
case string:
|
case string:
|
||||||
|
|
@ -113,7 +121,7 @@ func (p *AgentProvider) parseResult(result interface{}) ([]string, error) {
|
||||||
return keywords, nil
|
return keywords, nil
|
||||||
default:
|
default:
|
||||||
// Try to marshal and unmarshal
|
// Try to marshal and unmarshal
|
||||||
jsonBytes, err := json.Marshal(result)
|
jsonBytes, err := json.Marshal(next)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{}, nil
|
return []string{}, nil
|
||||||
}
|
}
|
||||||
|
|
@ -122,18 +130,6 @@ func (p *AgentProvider) parseResult(result interface{}) ([]string, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for "next" field (custom hook data from NextHookResponse)
|
|
||||||
// Stream() returns: { next: { data: { keywords: [...] } } }
|
|
||||||
if next, hasNext := data["next"]; hasNext && next != nil {
|
|
||||||
if nextMap, ok := next.(map[string]interface{}); ok {
|
|
||||||
data = nextMap
|
|
||||||
} else if nextStr, ok := next.(string); ok {
|
|
||||||
if err := json.Unmarshal([]byte(nextStr), &data); err != nil {
|
|
||||||
return []string{}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract keywords from data
|
// Extract keywords from data
|
||||||
// Try common field names: "keywords", "data", "data.keywords"
|
// Try common field names: "keywords", "data", "data.keywords"
|
||||||
if kw, ok := data["keywords"]; ok {
|
if kw, ok := data["keywords"]; ok {
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,8 @@ func (p *AgentProvider) Generate(ctx *agentContext.Context, input *Input) (*Resu
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the result
|
// Parse the result from response
|
||||||
genResult, err := p.parseResult(result)
|
genResult, err := p.parseResponse(result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lastError = err
|
lastError = err
|
||||||
continue
|
continue
|
||||||
|
|
@ -155,28 +155,36 @@ func (p *AgentProvider) validateDSL(dsl *gou.QueryDSL) *linter.LintResult {
|
||||||
return lintResult
|
return lintResult
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseResult extracts QueryDSL from the agent's response
|
// parseResponse extracts QueryDSL from the agent's *context.Response
|
||||||
// The querydsl agent returns QueryDSL JSON directly (not wrapped in {dsl: ...})
|
// Now that agent.Stream() returns *context.Response directly,
|
||||||
|
// we can access fields without type assertions.
|
||||||
|
//
|
||||||
|
// The querydsl agent returns QueryDSL in response.Next field
|
||||||
// Or returns error JSON: {"error": "code", "message": "..."}
|
// Or returns error JSON: {"error": "code", "message": "..."}
|
||||||
// Stream() returns *context.Response with QueryDSL in "next" field
|
func (p *AgentProvider) parseResponse(response *agentContext.Response) (*Result, error) {
|
||||||
func (p *AgentProvider) parseResult(result interface{}) (*Result, error) {
|
if response == nil {
|
||||||
if result == nil {
|
|
||||||
return &Result{}, nil
|
return &Result{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle *context.Response directly (most common case from Stream())
|
// Check Next field first (custom hook data)
|
||||||
if resp, ok := result.(*agentContext.Response); ok {
|
if response.Next != nil {
|
||||||
if resp.Next != nil {
|
return p.parseNextData(response.Next)
|
||||||
// Next contains the hook response, recursively parse it
|
}
|
||||||
return p.parseResult(resp.Next)
|
|
||||||
}
|
// No Next data, return empty result
|
||||||
|
return &Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseNextData extracts QueryDSL from Next hook data
|
||||||
|
func (p *AgentProvider) parseNextData(next interface{}) (*Result, error) {
|
||||||
|
if next == nil {
|
||||||
return &Result{}, nil
|
return &Result{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to convert to map first
|
// Try to convert to map first
|
||||||
var data map[string]interface{}
|
var data map[string]interface{}
|
||||||
|
|
||||||
switch v := result.(type) {
|
switch v := next.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
data = v
|
data = v
|
||||||
case string:
|
case string:
|
||||||
|
|
@ -186,7 +194,7 @@ func (p *AgentProvider) parseResult(result interface{}) (*Result, error) {
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// Try to marshal and unmarshal
|
// Try to marshal and unmarshal
|
||||||
jsonBytes, err := json.Marshal(result)
|
jsonBytes, err := json.Marshal(next)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &Result{}, nil
|
return &Result{}, nil
|
||||||
}
|
}
|
||||||
|
|
@ -197,18 +205,6 @@ func (p *AgentProvider) parseResult(result interface{}) (*Result, error) {
|
||||||
|
|
||||||
genResult := &Result{}
|
genResult := &Result{}
|
||||||
|
|
||||||
// Check for Stream() wrapper: { content: "...", next: {...} }
|
|
||||||
// The actual response is in "content" field as a string
|
|
||||||
if content, hasContent := data["content"]; hasContent && content != nil {
|
|
||||||
if contentStr, ok := content.(string); ok && contentStr != "" {
|
|
||||||
// Parse the content string as JSON
|
|
||||||
var contentData map[string]interface{}
|
|
||||||
if err := json.Unmarshal([]byte(contentStr), &contentData); err == nil {
|
|
||||||
data = contentData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for error response: {"error": "code", "message": "..."}
|
// Check for error response: {"error": "code", "message": "..."}
|
||||||
if errCode, hasError := data["error"]; hasError {
|
if errCode, hasError := data["error"]; hasError {
|
||||||
errMsg := ""
|
errMsg := ""
|
||||||
|
|
@ -229,22 +225,6 @@ func (p *AgentProvider) parseResult(result interface{}) (*Result, error) {
|
||||||
return genResult, nil
|
return genResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: check for wrapped formats
|
|
||||||
// Check for "next" field (custom hook data from NextHookResponse)
|
|
||||||
if next, hasNext := data["next"]; hasNext && next != nil {
|
|
||||||
if nextMap, ok := next.(map[string]interface{}); ok {
|
|
||||||
data = nextMap
|
|
||||||
} else if nextStr, ok := next.(string); ok {
|
|
||||||
if err := json.Unmarshal([]byte(nextStr), &data); err == nil {
|
|
||||||
// Check if parsed data is a QueryDSL
|
|
||||||
if _, hasFrom := data["from"]; hasFrom {
|
|
||||||
genResult.DSL = p.extractDSL(data)
|
|
||||||
return genResult, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for "dsl" field wrapper: { dsl: {...} }
|
// Check for "dsl" field wrapper: { dsl: {...} }
|
||||||
if dsl, ok := data["dsl"]; ok {
|
if dsl, ok := data["dsl"]; ok {
|
||||||
genResult.DSL = p.extractDSL(dsl)
|
genResult.DSL = p.extractDSL(dsl)
|
||||||
|
|
|
||||||
|
|
@ -59,20 +59,24 @@ func (p *AgentProvider) Rerank(ctx *context.Context, query string, items []*type
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := agent.Stream(ctx, messages, options)
|
response, err := agent.Stream(ctx, messages, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("agent stream failed: %w", err)
|
return nil, fmt.Errorf("agent stream failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse response
|
// Parse response from response.Next
|
||||||
return p.parseResponse(result, items, opts)
|
return p.parseAgentResponse(response, items, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseResponse extracts reranked items from agent response
|
// parseAgentResponse extracts reranked items from agent's *context.Response
|
||||||
// The response format from agent.Stream is typically:
|
// Now that agent.Stream() returns *context.Response directly,
|
||||||
// { "next": { "data": { "order": [...] } } }
|
// we can access fields without type assertions.
|
||||||
func (p *AgentProvider) parseResponse(result interface{}, originalItems []*types.ResultItem, opts *types.RerankOptions) ([]*types.ResultItem, error) {
|
//
|
||||||
if result == nil {
|
// Expected response.Next format:
|
||||||
|
// { "order": ["ref_001", "ref_003", "ref_002"] }
|
||||||
|
// Or: { "items": [{ "citation_id": "ref_001", ... }, ...] }
|
||||||
|
func (p *AgentProvider) parseAgentResponse(response *context.Response, originalItems []*types.ResultItem, opts *types.RerankOptions) ([]*types.ResultItem, error) {
|
||||||
|
if response == nil || response.Next == nil {
|
||||||
return originalItems, nil
|
return originalItems, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,20 +88,20 @@ func (p *AgentProvider) parseResponse(result interface{}, originalItems []*types
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract response data
|
// Extract response data from Next field
|
||||||
response := extractResponseData(result)
|
data := extractNextData(response.Next)
|
||||||
if response == nil {
|
if data == nil {
|
||||||
return originalItems, nil
|
return originalItems, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get reranked order from response
|
// Try to get reranked order from data
|
||||||
// Expected format: { "order": ["ref_001", "ref_003", "ref_002"] }
|
// Expected format: { "order": ["ref_001", "ref_003", "ref_002"] }
|
||||||
// Or: { "items": [{ "citation_id": "ref_001", ... }, ...] }
|
// Or: { "items": [{ "citation_id": "ref_001", ... }, ...] }
|
||||||
|
|
||||||
var reranked []*types.ResultItem
|
var reranked []*types.ResultItem
|
||||||
|
|
||||||
// Try "order" field (list of citation IDs)
|
// Try "order" field (list of citation IDs)
|
||||||
if order, ok := response["order"]; ok {
|
if order, ok := data["order"]; ok {
|
||||||
if orderList := toStringSlice(order); len(orderList) > 0 {
|
if orderList := toStringSlice(order); len(orderList) > 0 {
|
||||||
for _, id := range orderList {
|
for _, id := range orderList {
|
||||||
if item, exists := itemMap[id]; exists {
|
if item, exists := itemMap[id]; exists {
|
||||||
|
|
@ -116,7 +120,7 @@ func (p *AgentProvider) parseResponse(result interface{}, originalItems []*types
|
||||||
|
|
||||||
// Try "items" field (full items or items with citation_id)
|
// Try "items" field (full items or items with citation_id)
|
||||||
if len(reranked) == 0 {
|
if len(reranked) == 0 {
|
||||||
if items, ok := response["items"]; ok {
|
if items, ok := data["items"]; ok {
|
||||||
if itemsList := toItemsList(items); len(itemsList) > 0 {
|
if itemsList := toItemsList(items); len(itemsList) > 0 {
|
||||||
for _, respItem := range itemsList {
|
for _, respItem := range itemsList {
|
||||||
// Check if it's just a reference or full item
|
// Check if it's just a reference or full item
|
||||||
|
|
@ -150,20 +154,16 @@ func (p *AgentProvider) parseResponse(result interface{}, originalItems []*types
|
||||||
return reranked, nil
|
return reranked, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractResponseData extracts the actual response data from agent.Stream result
|
// extractNextData extracts the actual data from response.Next field
|
||||||
// Handles nested structures like { "next": { "data": { ... } } }
|
// Handles nested structures like { "data": { ... } }
|
||||||
func extractResponseData(result interface{}) map[string]interface{} {
|
func extractNextData(next interface{}) map[string]interface{} {
|
||||||
switch v := result.(type) {
|
if next == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := next.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
// Check for "next" wrapper (from NextHookResponse)
|
// Check for "data" wrapper
|
||||||
if next, ok := v["next"].(map[string]interface{}); ok {
|
|
||||||
// Check for "data" inside next
|
|
||||||
if data, ok := next["data"].(map[string]interface{}); ok {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
// Check for direct "data" wrapper
|
|
||||||
if data, ok := v["data"].(map[string]interface{}); ok {
|
if data, ok := v["data"].(map[string]interface{}); ok {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
@ -172,16 +172,14 @@ func extractResponseData(result interface{}) map[string]interface{} {
|
||||||
// Try to parse as JSON
|
// Try to parse as JSON
|
||||||
var data map[string]interface{}
|
var data map[string]interface{}
|
||||||
if err := json.Unmarshal([]byte(v), &data); err == nil {
|
if err := json.Unmarshal([]byte(v), &data); err == nil {
|
||||||
return extractResponseData(data)
|
return extractNextData(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Try to handle other types by converting to JSON and back
|
// Try to handle other types by converting to JSON and back
|
||||||
if result != nil {
|
if bytes, err := json.Marshal(next); err == nil {
|
||||||
if bytes, err := json.Marshal(result); err == nil {
|
var data map[string]interface{}
|
||||||
var data map[string]interface{}
|
if err := json.Unmarshal(bytes, &data); err == nil {
|
||||||
if err := json.Unmarshal(bytes, &data); err == nil {
|
return extractNextData(data)
|
||||||
return extractResponseData(data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -532,26 +532,23 @@ func buildContextOptions(tc *Case, runnerOpts *Options) *context.Options {
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractOutput extracts the output from the agent response
|
// extractOutput extracts the output from the agent response
|
||||||
// Priority: Next hook data (if non-empty) > Completion content > raw response
|
// Priority: Next hook data (if non-empty) > Completion content > nil
|
||||||
func extractOutput(response interface{}) interface{} {
|
func extractOutput(response *context.Response) interface{} {
|
||||||
if response == nil {
|
if response == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get data from context.Response
|
// Prefer Next hook data if available and non-empty
|
||||||
if resp, ok := response.(*context.Response); ok {
|
// response.Next is already the Data value (not NextHookResponse struct)
|
||||||
// Prefer Next hook data if available and non-empty
|
if response.Next != nil && !isEmptyValue(response.Next) {
|
||||||
// resp.Next is already the Data value (not NextHookResponse struct)
|
return response.Next
|
||||||
if resp.Next != nil && !isEmptyValue(resp.Next) {
|
}
|
||||||
return resp.Next
|
// Fall back to raw completion content
|
||||||
}
|
if response.Completion != nil {
|
||||||
// Fall back to raw completion content
|
return response.Completion.Content
|
||||||
if resp.Completion != nil {
|
|
||||||
return resp.Completion.Content
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isEmptyValue checks if a value is considered "empty" for output purposes
|
// isEmptyValue checks if a value is considered "empty" for output purposes
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue