mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
235 lines
7.8 KiB
Go
235 lines
7.8 KiB
Go
// Package providers contains AI provider client implementations
|
|
package providers
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/agentcapabilities"
|
|
)
|
|
|
|
// Message represents a chat message
|
|
type Message struct {
|
|
Role string `json:"role"` // "user", "assistant", "system"
|
|
Content string `json:"content"` // Text content (simple case)
|
|
ReasoningContent string `json:"reasoning_content,omitempty"` // DeepSeek thinking mode
|
|
ToolCalls []ToolCall `json:"tool_calls"` // For assistant messages with tool calls
|
|
ToolResult *ToolResult `json:"tool_result,omitempty"` // For user messages with tool results
|
|
}
|
|
|
|
func EmptyMessage() Message {
|
|
return Message{}.NormalizeCollections()
|
|
}
|
|
|
|
func (m Message) NormalizeCollections() Message {
|
|
if m.ToolCalls == nil {
|
|
m.ToolCalls = []ToolCall{}
|
|
}
|
|
for i := range m.ToolCalls {
|
|
m.ToolCalls[i] = m.ToolCalls[i].NormalizeCollections()
|
|
}
|
|
return m
|
|
}
|
|
|
|
// ToolCall represents a tool invocation from the AI. It aliases the shared
|
|
// Pulse Intelligence provider-call shape so Assistant execution projects
|
|
// model-selected calls through the same boundary as external agent adapters.
|
|
type ToolCall = agentcapabilities.ProviderToolCall
|
|
|
|
func EmptyToolCall() ToolCall {
|
|
return agentcapabilities.EmptyProviderToolCall()
|
|
}
|
|
|
|
// ToolResult represents the result of a tool execution.
|
|
type ToolResult = agentcapabilities.ProviderToolResult
|
|
|
|
// Tool represents an AI tool definition. It aliases the shared Pulse
|
|
// Intelligence provider tool shape so Assistant provider clients project tools
|
|
// through the same schema boundary.
|
|
type Tool = agentcapabilities.ProviderTool
|
|
|
|
func EmptyTool() Tool {
|
|
return agentcapabilities.EmptyProviderTool()
|
|
}
|
|
|
|
// ToolChoiceType represents how the model should choose tools
|
|
type ToolChoiceType string
|
|
|
|
const (
|
|
// ToolChoiceNone prevents the model from using any tools
|
|
ToolChoiceNone ToolChoiceType = "none"
|
|
// ToolChoiceRequired asks providers with a native forcing mechanism to
|
|
// require at least one tool call.
|
|
ToolChoiceRequired ToolChoiceType = "required"
|
|
)
|
|
|
|
// ToolChoice controls how the model selects tools
|
|
type ToolChoice struct {
|
|
Type ToolChoiceType `json:"type"`
|
|
}
|
|
|
|
// ChatRequest represents a request to the AI provider
|
|
type ChatRequest struct {
|
|
Messages []Message `json:"messages"`
|
|
Model string `json:"model"`
|
|
ExecutionID string `json:"execution_id,omitempty"` // Stable higher-level run ID shared across related provider turns
|
|
MaxTokens int `json:"max_tokens,omitempty"`
|
|
Temperature float64 `json:"temperature,omitempty"`
|
|
System string `json:"system,omitempty"` // System prompt (Anthropic style)
|
|
Tools []Tool `json:"tools,omitempty"` // Available tools
|
|
ToolChoice *ToolChoice `json:"tool_choice,omitempty"` // nil = model-owned automatic selection; none = text-only safety brake; required = provider-native forced tool use where supported
|
|
}
|
|
|
|
func (r ChatRequest) NormalizeCollections() ChatRequest {
|
|
if r.Messages == nil {
|
|
r.Messages = []Message{}
|
|
}
|
|
for i := range r.Messages {
|
|
r.Messages[i] = r.Messages[i].NormalizeCollections()
|
|
}
|
|
if r.Tools == nil {
|
|
r.Tools = []Tool{}
|
|
}
|
|
for i := range r.Tools {
|
|
r.Tools[i] = r.Tools[i].NormalizeCollections()
|
|
}
|
|
return r
|
|
}
|
|
|
|
// ChatResponse represents a response from the AI provider
|
|
type ChatResponse struct {
|
|
Content string `json:"content"`
|
|
ReasoningContent string `json:"reasoning_content,omitempty"` // DeepSeek thinking mode
|
|
Model string `json:"model"`
|
|
StopReason string `json:"stop_reason,omitempty"` // "end_turn", "tool_use"
|
|
ToolCalls []ToolCall `json:"tool_calls"` // Tool invocations
|
|
InputTokens int `json:"input_tokens,omitempty"`
|
|
OutputTokens int `json:"output_tokens,omitempty"`
|
|
}
|
|
|
|
func (r ChatResponse) NormalizeCollections() ChatResponse {
|
|
if r.ToolCalls == nil {
|
|
r.ToolCalls = []ToolCall{}
|
|
}
|
|
for i := range r.ToolCalls {
|
|
r.ToolCalls[i] = r.ToolCalls[i].NormalizeCollections()
|
|
}
|
|
return r
|
|
}
|
|
|
|
// ModelInfo represents information about an available model
|
|
type ModelInfo struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
CreatedAt int64 `json:"created_at,omitempty"`
|
|
Notable bool `json:"notable"` // Whether this is a "latest and greatest" model
|
|
Provider string `json:"provider,omitempty"`
|
|
}
|
|
|
|
// Provider defines the interface for AI providers
|
|
type Provider interface {
|
|
// Chat sends a chat request and returns the response
|
|
Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error)
|
|
|
|
// TestConnection validates the API key and connectivity
|
|
TestConnection(ctx context.Context) error
|
|
|
|
// Name returns the provider name
|
|
Name() string
|
|
|
|
// ListModels returns available models from the provider's API
|
|
// Returns nil if the provider doesn't support listing models
|
|
ListModels(ctx context.Context) ([]ModelInfo, error)
|
|
}
|
|
|
|
// StreamEvent represents a streaming event from the AI provider
|
|
type StreamEvent struct {
|
|
Type string // "content", "thinking", "tool_start", "tool_progress", "tool_end", "done", "error"
|
|
Data interface{} // Type-specific data
|
|
}
|
|
|
|
// ContentEvent is the data for "content" stream events
|
|
type ContentEvent struct {
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
// ThinkingEvent is the data for "thinking" stream events (extended thinking/reasoning)
|
|
type ThinkingEvent struct {
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
// ToolStartEvent is the data for "tool_start" stream events
|
|
type ToolStartEvent struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Input map[string]interface{} `json:"input"`
|
|
}
|
|
|
|
func (e ToolStartEvent) NormalizeCollections() ToolStartEvent {
|
|
if e.Input == nil {
|
|
e.Input = map[string]interface{}{}
|
|
}
|
|
return e
|
|
}
|
|
|
|
// ToolProgressEvent is the data for "tool_progress" stream events.
|
|
// Providers use this to mutate an already visible pending tool row while the
|
|
// model is still streaming tool arguments or while execution advances.
|
|
type ToolProgressEvent struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Input map[string]interface{} `json:"input,omitempty"`
|
|
RawInput string `json:"raw_input,omitempty"`
|
|
Phase string `json:"phase,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
func (e ToolProgressEvent) NormalizeCollections() ToolProgressEvent {
|
|
if e.Input == nil {
|
|
e.Input = map[string]interface{}{}
|
|
}
|
|
return e
|
|
}
|
|
|
|
// ToolEndEvent is the data for "tool_end" stream events
|
|
type ToolEndEvent struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Result string `json:"result,omitempty"`
|
|
IsError bool `json:"is_error,omitempty"`
|
|
}
|
|
|
|
// ErrorEvent is the data for "error" stream events
|
|
type ErrorEvent struct {
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// DoneEvent is the data for "done" stream events
|
|
type DoneEvent struct {
|
|
StopReason string `json:"stop_reason,omitempty"`
|
|
ToolCalls []ToolCall `json:"tool_calls"`
|
|
InputTokens int `json:"input_tokens,omitempty"`
|
|
OutputTokens int `json:"output_tokens,omitempty"`
|
|
}
|
|
|
|
func (e DoneEvent) NormalizeCollections() DoneEvent {
|
|
if e.ToolCalls == nil {
|
|
e.ToolCalls = []ToolCall{}
|
|
}
|
|
for i := range e.ToolCalls {
|
|
e.ToolCalls[i] = e.ToolCalls[i].NormalizeCollections()
|
|
}
|
|
return e
|
|
}
|
|
|
|
// StreamCallback is called for each streaming event
|
|
type StreamCallback func(event StreamEvent)
|
|
|
|
// StreamingProvider extends Provider with streaming support
|
|
type StreamingProvider interface {
|
|
Provider
|
|
// ChatStream sends a chat request and streams the response via callback
|
|
ChatStream(ctx context.Context, req ChatRequest, callback StreamCallback) error
|
|
// SupportsThinking returns true if the model supports extended thinking/reasoning
|
|
SupportsThinking(model string) bool
|
|
}
|