mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Clears the sixteen dupl pair groups in internal/api plus the pkg/pulsecli pair: - router.go: privileged settings endpoints share the serveSetupTokenOrSettingsWrite gate; patrol findings convert via one unifiedFindingFromAI; the five infrastructure-summary chart loops share collectGuestChartData / fillChartSeriesFromBatch; the VM/LXC workloads summary loops share appendGuestWorkloadSummaries. - deploy_handlers.go: preflight and job status/SSE handlers share handleDeployJobStatus / handleDeployJobEvents. - recovery_handlers.go: points series/facets share parseRecoveryListPointsOptions. - truenas_handlers.go / vmware_handlers.go / router_routes_registration.go: the connection update flow (locate, decode-with-fallback, normalize, preserve masked secrets, validate, save, redact) moves to the new platform_connection_shared.go (updatePlatformConnection + decodeOptionalInstanceRequest + the admin-gated item-route builder); per-platform wrappers carry nolint'd declarative wiring only. - docker_metadata.go / guest_metadata.go: GET/PUT payload semantics move to metadata_handlers_shared.go; the zero-record-instead-of-404 contract is pinned by TestContract_MetadataGetPayloadsUseZeroRecordsInsteadOf404. - kubernetes_agents.go / docker_agents.go: lifecycle PUTs share per-handler action helpers. - config_node_handlers.go: PBS/PMG probes share testProxmoxPlatformConnection. - cloud_handoff_handlers.go / purchase_return_redemptions.go: secrets sqlite stores open through openHardenedSecretsDB so permission hardening stays single-sourced. - ai_handlers.go / chat_service_adapter.go: the GetMessages adapters are deliberate contract mirrors — suppressed with nolint and enforced by TestOrchestratorAndChatAdaptersMapTheSameMessageFields. - pkg/pulsecli/actions.go: action subcommands seed env defaults via actionAPIDefaults (tested); the audit/events cobra registration pair is nolint'd parallel wiring. - .golangci.yml: exclude gitignored tmp/ from ./... typechecking. - subsystem_lookup_test.py: refresh the pinned api-contracts.md line numbers shifted by the contract additions. golangci-lint run ./... is now fully green. Full internal/api and pkg/pulsecli test suites pass.
124 lines
3.9 KiB
Go
124 lines
3.9 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/chat"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/providers"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/tools"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
)
|
|
|
|
// chatServiceAdapter wraps chat.Service to implement ai.ChatServiceProvider.
|
|
// This bridges the chat package (concrete) to the ai package (interface) without
|
|
// creating an import cycle.
|
|
type chatServiceAdapter struct {
|
|
svc *chat.Service
|
|
}
|
|
|
|
func (a *chatServiceAdapter) CreateSession(ctx context.Context) (*ai.ChatSession, error) {
|
|
session, err := a.svc.CreateSession(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &ai.ChatSession{ID: session.ID}, nil
|
|
}
|
|
|
|
func (a *chatServiceAdapter) ExecuteStream(ctx context.Context, req ai.ChatExecuteRequest, callback ai.ChatStreamCallback) error {
|
|
return a.svc.ExecuteStream(ctx, chat.ExecuteRequest{
|
|
Prompt: req.Prompt,
|
|
SessionID: req.SessionID,
|
|
}, adaptCallback(callback))
|
|
}
|
|
|
|
func (a *chatServiceAdapter) ExecutePatrolStream(ctx context.Context, req ai.PatrolExecuteRequest, callback ai.ChatStreamCallback) (*ai.PatrolStreamResponse, error) {
|
|
resp, err := a.svc.ExecutePatrolStream(ctx, chat.PatrolRequest{
|
|
Prompt: req.Prompt,
|
|
SystemPrompt: req.SystemPrompt,
|
|
SessionID: req.SessionID,
|
|
ExecutionID: req.ExecutionID,
|
|
UseCase: req.UseCase,
|
|
MaxTurns: req.MaxTurns,
|
|
}, adaptCallback(callback))
|
|
if err != nil {
|
|
if resp != nil {
|
|
return &ai.PatrolStreamResponse{
|
|
Content: resp.Content,
|
|
InputTokens: resp.InputTokens,
|
|
OutputTokens: resp.OutputTokens,
|
|
}, err
|
|
}
|
|
return nil, err
|
|
}
|
|
return &ai.PatrolStreamResponse{
|
|
Content: resp.Content,
|
|
InputTokens: resp.InputTokens,
|
|
OutputTokens: resp.OutputTokens,
|
|
}, nil
|
|
}
|
|
|
|
//nolint:dupl // mirrors orchestratorChatAdapter.GetMessages: same source messages mapped onto a deliberately separate output contract that may diverge
|
|
func (a *chatServiceAdapter) GetMessages(ctx context.Context, sessionID string) ([]ai.ChatMessage, error) {
|
|
messages, err := a.svc.GetMessages(ctx, sessionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]ai.ChatMessage, len(messages))
|
|
for i, m := range messages {
|
|
msg := ai.ChatMessage{
|
|
ID: m.ID,
|
|
Role: m.Role,
|
|
Content: m.Content,
|
|
ReasoningContent: m.ReasoningContent,
|
|
Timestamp: m.Timestamp,
|
|
}
|
|
for _, tc := range m.ToolCalls {
|
|
msg.ToolCalls = append(msg.ToolCalls, ai.ChatToolCall{
|
|
ID: tc.ID,
|
|
Name: tc.Name,
|
|
Input: tc.Input,
|
|
})
|
|
}
|
|
if m.ToolResult != nil {
|
|
msg.ToolResult = &ai.ChatToolResult{
|
|
ToolUseID: m.ToolResult.ToolUseID,
|
|
Content: m.ToolResult.Content,
|
|
IsError: m.ToolResult.IsError,
|
|
}
|
|
}
|
|
result[i] = msg.NormalizeCollections()
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (a *chatServiceAdapter) DeleteSession(ctx context.Context, sessionID string) error {
|
|
return a.svc.DeleteSession(ctx, sessionID)
|
|
}
|
|
|
|
func (a *chatServiceAdapter) ReloadConfig(ctx context.Context, cfg *config.AIConfig) error {
|
|
return a.svc.Restart(ctx, cfg)
|
|
}
|
|
|
|
func (a *chatServiceAdapter) SetPatrolProviderFactory(factory func(string) (providers.StreamingProvider, error)) {
|
|
a.svc.SetPatrolProviderFactory(factory)
|
|
}
|
|
|
|
// GetExecutor exposes the underlying chat service's tool executor so that
|
|
// patrol can set the finding creator. This satisfies the
|
|
// chatServiceExecutorAccessor interface in the ai package.
|
|
func (a *chatServiceAdapter) GetExecutor() *tools.PulseToolExecutor {
|
|
return a.svc.GetExecutor()
|
|
}
|
|
|
|
// adaptCallback converts an ai.ChatStreamCallback to a chat.StreamCallback.
|
|
// The ai package uses []byte for event data, while the chat package uses json.RawMessage.
|
|
func adaptCallback(callback ai.ChatStreamCallback) chat.StreamCallback {
|
|
return func(event chat.StreamEvent) {
|
|
callback(ai.ChatStreamEvent{
|
|
Type: event.Type,
|
|
Data: json.RawMessage(event.Data),
|
|
})
|
|
}
|
|
}
|