Files
2025-12-19 23:05:04 +01:00

555 lines
16 KiB
Go

package types
import (
"encoding/json"
"fmt"
"strings"
coreTypes "github.com/mudler/LocalAGI/core/types"
"github.com/mudler/xlog"
"github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema"
)
// Input represents either a string or a slice of Message
type Input struct {
Text *string `json:"-"`
Messages *[]Message `json:"-"`
}
// UnmarshalJSON implements custom JSON unmarshaling for Input
func (i *Input) UnmarshalJSON(data []byte) error {
// Try to unmarshal as string first
var text string
if err := json.Unmarshal(data, &text); err == nil {
i.Text = &text
return nil
}
// Try to unmarshal as []Message
var messages []Message
if err := json.Unmarshal(data, &messages); err == nil {
i.Messages = &messages
return nil
}
return json.Unmarshal(data, &struct{}{}) // fallback to empty struct
}
// MarshalJSON implements custom JSON marshaling for Input
func (i *Input) MarshalJSON() ([]byte, error) {
if i.Text != nil {
return json.Marshal(*i.Text)
}
if i.Messages != nil {
return json.Marshal(*i.Messages)
}
return json.Marshal(nil)
}
// IsText returns true if the input contains text
func (i *Input) IsText() bool {
return i.Text != nil
}
// IsMessages returns true if the input contains messages
func (i *Input) IsMessages() bool {
return i.Messages != nil
}
// GetText returns the text value or empty string
func (i *Input) GetText() string {
if i.Text != nil {
return *i.Text
}
return ""
}
// GetMessages returns the messages value or empty slice
func (i *Input) GetMessages() []Message {
if i.Messages != nil {
return *i.Messages
}
return nil
}
// Message represents different types of messages in the input
type Message struct {
// Common fields
Type string `json:"type,omitempty"`
// InputMessage fields (when this is a regular chat message)
Role *string `json:"role,omitempty"`
Content *Content `json:"content,omitempty"`
// WebSearchToolCall fields (when type == "web_search_call")
ID *string `json:"id,omitempty"`
Status *string `json:"status,omitempty"`
// Function call and function call output
Arguments *string `json:"arguments,omitempty"`
CallId *string `json:"call_id,omitempty"`
Name *string `json:"name,omitempty"`
Output *string `json:"output,omitempty"`
}
// IsInputMessage returns true if this is a regular chat message
func (m *Message) IsInputMessage() bool {
return m.Role != nil
}
// IsWebSearchCall returns true if this is a web search tool call
func (m *Message) IsWebSearchCall() bool {
return m.Type == "web_search_call"
}
func (m *Message) IsFunctionCall() bool {
return m.Type == "function_call"
}
func (m *Message) IsFunctionCallOutput() bool {
return m.Type == "function_call_output"
}
func (m *Message) IsValidFunctionCallOutput() (bool, error) {
var errors []string
if m.Type != "function_call_output" {
errors = append(errors, "Type should be 'function_call_output'")
}
if m.CallId == nil {
errors = append(errors, "CallId not found")
}
if m.Output == nil {
errors = append(errors, "Output not found")
}
return len(errors) < 1, fmt.Errorf("Invalid function_call_output message: %s", strings.Join(errors, ", "))
}
// ToInputMessage converts to InputMessage if this is a regular message
func (m *Message) ToInputMessage() *InputMessage {
if m.IsInputMessage() && m.Role != nil && m.Content != nil {
content := *m.Content
return &InputMessage{
Role: *m.Role,
Content: content,
}
}
return nil
}
type ToolChoice struct {
Name string `json:"name"`
Type string `json:"type"`
}
// RequestBody represents the request body structure for the OpenAI API
type RequestBody struct {
Model string `json:"model"`
Input Input `json:"input"`
Include []string `json:"include,omitempty"`
Instructions *string `json:"instructions,omitempty"`
MaxOutputTokens *int `json:"max_output_tokens,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`
PreviousResponseID *string `json:"previous_response_id,omitempty"`
Reasoning *ReasoningConfig `json:"reasoning,omitempty"`
Store *bool `json:"store,omitempty"`
Stream *bool `json:"stream,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
Text *TextConfig `json:"text,omitempty"`
ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
Tools []Tool `json:"tools,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
Truncation *string `json:"truncation,omitempty"`
}
func (r *RequestBody) SetInputByType() {
// This method is no longer needed as Input handles unmarshaling automatically
if r.Input.IsText() {
xlog.Debug("[Parse Request] Set input type as text", "input", r.Input.GetText())
} else if r.Input.IsMessages() {
xlog.Debug("[Parse Request] Input messages parsed", "messages", r.Input.GetMessages())
}
}
func (r *RequestBody) ToChatCompletionMessages() []openai.ChatCompletionMessage {
result := []openai.ChatCompletionMessage{}
if r.Input.IsMessages() {
for _, m := range r.Input.GetMessages() {
if m.IsFunctionCall() {
result = append(result, openai.ChatCompletionMessage{
Role: "assistant",
ToolCalls: []openai.ToolCall{
{
Type: "function",
ID: *m.CallId,
Function: openai.FunctionCall{
Arguments: *m.Arguments,
Name: *m.Name,
},
},
},
})
}
if m.IsFunctionCallOutput() {
if ok, err := m.IsValidFunctionCallOutput(); !ok {
xlog.Debug("Error processing input message", "error", err)
} else {
result = append(result, openai.ChatCompletionMessage{
Role: "tool",
Content: *m.Output,
ToolCallID: *m.CallId,
})
}
}
if !m.IsInputMessage() {
continue
}
content := []openai.ChatMessagePart{}
oneImageWasFound := false
if m.Content != nil && m.Content.IsText() && m.Content.GetText() != "" {
content = append(content, openai.ChatMessagePart{
Type: "text",
Text: m.Content.GetText(),
})
}
if m.Content != nil && m.Content.IsItems() {
for _, c := range m.Content.GetItems() {
switch c.Type {
case "text":
content = append(content, openai.ChatMessagePart{
Type: "text",
Text: c.Text,
})
case "image":
oneImageWasFound = true
content = append(content, openai.ChatMessagePart{
Type: "image",
ImageURL: &openai.ChatMessageImageURL{URL: c.ImageURL},
})
}
}
}
if oneImageWasFound {
result = append(result, openai.ChatCompletionMessage{
Role: *m.Role,
MultiContent: content,
})
} else {
for _, c := range content {
result = append(result, openai.ChatCompletionMessage{
Role: *m.Role,
Content: c.Text,
})
}
}
}
}
if r.Input.IsText() && r.Input.GetText() != "" {
result = append(result, openai.ChatCompletionMessage{
Role: "user",
Content: r.Input.GetText(),
})
}
return result
}
// ReasoningConfig represents reasoning configuration options
type ReasoningConfig struct {
Effort *string `json:"effort,omitempty"`
Summary *string `json:"summary,omitempty"`
}
// TextConfig represents text configuration options
type TextConfig struct {
Format *FormatConfig `json:"format,omitempty"`
}
// FormatConfig represents format configuration options
type FormatConfig struct {
Type string `json:"type"`
}
// ResponseMessage represents a message in the response
type ResponseMessage struct {
Type string `json:"type"`
ID string `json:"id"`
Status string `json:"status"`
Role string `json:"role"`
Content []MessageContentItem `json:"content"`
}
// MessageContentItem represents a content item in a message
type MessageContentItem struct {
Type string `json:"type"`
Text string `json:"text"`
Annotations []interface{} `json:"annotations"`
}
// FunctionToolCall represents a function tool call as a top-level object in the output array
type FunctionToolCall struct {
Arguments string `json:"arguments"`
CallID string `json:"call_id"`
Name string `json:"name"`
Type string `json:"type"`
ID string `json:"id"`
Status string `json:"status"`
}
// UsageInfo represents token usage information
type UsageInfo struct {
InputTokens int `json:"input_tokens"`
InputTokensDetails TokenDetails `json:"input_tokens_details"`
OutputTokens int `json:"output_tokens"`
OutputTokensDetails TokenDetails `json:"output_tokens_details"`
TotalTokens int `json:"total_tokens"`
}
// TokenDetails represents details about token usage
type TokenDetails struct {
CachedTokens int `json:"cached_tokens"`
ReasoningTokens int `json:"reasoning_tokens,omitempty"`
}
// ResponseBody represents the structure of the OpenAI API response
type ResponseBody struct {
ID string `json:"id"`
Object string `json:"object"`
CreatedAt int64 `json:"created_at"`
Status string `json:"status"`
Error interface{} `json:"error"`
IncompleteDetails interface{} `json:"incomplete_details"`
Instructions interface{} `json:"instructions"`
MaxOutputTokens interface{} `json:"max_output_tokens"`
Model string `json:"model"`
Output []interface{} `json:"output"`
ParallelToolCalls bool `json:"parallel_tool_calls"`
PreviousResponseID interface{} `json:"previous_response_id"`
Reasoning ReasoningConfig `json:"reasoning"`
Store bool `json:"store"`
Temperature float64 `json:"temperature"`
Text TextConfig `json:"text"`
ToolChoice string `json:"tool_choice"`
Tools []Tool `json:"tools"`
TopP float64 `json:"top_p"`
Truncation string `json:"truncation"`
Usage UsageInfo `json:"usage"`
User interface{} `json:"user"`
Metadata map[string]interface{} `json:"metadata"`
}
// Content represents either a string or a slice of ContentItem
type Content struct {
Text *string `json:"-"`
Items *[]ContentItem `json:"-"`
}
// UnmarshalJSON implements custom JSON unmarshaling for Content
func (c *Content) UnmarshalJSON(data []byte) error {
// Try to unmarshal as string first
var text string
if err := json.Unmarshal(data, &text); err == nil {
c.Text = &text
return nil
}
// Try to unmarshal as []ContentItem
var items []ContentItem
if err := json.Unmarshal(data, &items); err == nil {
c.Items = &items
return nil
}
return json.Unmarshal(data, &struct{}{}) // fallback to empty struct
}
// MarshalJSON implements custom JSON marshaling for Content
func (c *Content) MarshalJSON() ([]byte, error) {
if c.Text != nil {
return json.Marshal(*c.Text)
}
if c.Items != nil {
return json.Marshal(*c.Items)
}
return json.Marshal(nil)
}
// IsText returns true if the content contains text
func (c *Content) IsText() bool {
return c.Text != nil
}
// IsItems returns true if the content contains items
func (c *Content) IsItems() bool {
return c.Items != nil
}
// GetText returns the text value or empty string
func (c *Content) GetText() string {
if c.Text != nil {
return *c.Text
}
return ""
}
// GetItems returns the items value or empty slice
func (c *Content) GetItems() []ContentItem {
if c.Items != nil {
return *c.Items
}
return nil
}
// InputMessage represents a user input message
type InputMessage struct {
Role string `json:"role"`
Content Content `json:"content"`
}
// ContentItem represents an item in a content array
type ContentItem struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
ImageURL string `json:"image_url,omitempty"`
}
// Tool represents a tool that can be called by the assistant
type Tool struct {
Type string `json:"type"`
// Function tool fields (used when type == "function")
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Parameters *jsonschema.Definition `json:"parameters,omitempty"`
Strict *bool `json:"strict,omitempty"`
// Web search tool fields (used when type == "web_search_preview" etc.)
SearchContextSize *string `json:"search_context_size,omitempty"`
UserLocation *UserLocation `json:"user_location,omitempty"`
}
// IsFunction returns true if this is a function tool
func (t *Tool) IsFunction() bool {
return t.Type == "function"
}
// IsWebSearch returns true if this is a web search tool
func (t *Tool) IsWebSearch() bool {
return t.Type == "web_search_preview" || t.Type == "web_search_preview_2025_03_11"
}
// ToActionDefinition converts this tool to an ActionDefinition
func (t *Tool) ToActionDefinition() *coreTypes.ActionDefinition {
if t.IsFunction() && t.Name != nil {
// Regular function tool
properties := make(map[string]jsonschema.Definition)
required := []string{}
if t.Parameters != nil {
properties = t.Parameters.Properties
required = t.Parameters.Required
}
desc := ""
if t.Description != nil {
desc = *t.Description
}
return &coreTypes.ActionDefinition{
Name: coreTypes.ActionDefinitionName(*t.Name),
Description: desc,
Properties: properties,
Required: required,
}
}
if t.IsWebSearch() {
// Convert web search builtin to ActionDefinition
name := "web_search_" + t.Type
desc := "Web search tool for finding relevant information online"
// Create parameters schema for web search options
properties := map[string]jsonschema.Definition{
"search_context_size": {
Type: jsonschema.String,
Enum: []string{"low", "medium", "high"},
Description: "Amount of context window space to use for search",
},
"user_location": {
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"type": {
Type: jsonschema.String,
Enum: []string{"approximate"},
Description: "Type of location approximation",
},
"city": {
Type: jsonschema.String,
Description: "City of the user",
},
"country": {
Type: jsonschema.String,
Description: "Two-letter ISO country code",
},
"region": {
Type: jsonschema.String,
Description: "Region of the user",
},
"timezone": {
Type: jsonschema.String,
Description: "IANA timezone of the user",
},
},
},
}
return &coreTypes.ActionDefinition{
Name: coreTypes.ActionDefinitionName(name),
Description: desc,
Properties: properties,
Required: []string{},
}
}
return nil
}
// SeparateTools separates a slice of Tools into builtin tools and user tools as ActionDefinitions
func SeparateTools(tools []Tool) (builtinTools []coreTypes.ActionDefinition, userTools []coreTypes.ActionDefinition) {
for _, tool := range tools {
if actionDef := tool.ToActionDefinition(); actionDef != nil {
if tool.IsFunction() {
// User-defined function tool
userTools = append(userTools, *actionDef)
} else if tool.IsWebSearch() {
// Builtin tool (web search)
builtinTools = append(builtinTools, *actionDef)
}
}
}
return builtinTools, userTools
}
// UserLocation represents the user's location for web search
type UserLocation struct {
Type string `json:"type"`
City *string `json:"city,omitempty"`
Country *string `json:"country,omitempty"`
Region *string `json:"region,omitempty"`
Timezone *string `json:"timezone,omitempty"`
}