initial commit
This commit is contained in:
192
internal/infrastructure/llm/lmstudio.go
Normal file
192
internal/infrastructure/llm/lmstudio.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/paramah/ai_devs4/s01e03/internal/domain"
|
||||
)
|
||||
|
||||
// LMStudioProvider implements domain.LLMProvider for local LM Studio
|
||||
type LMStudioProvider struct {
|
||||
baseURL string
|
||||
model string
|
||||
client *http.Client
|
||||
verbose bool
|
||||
}
|
||||
|
||||
// NewLMStudioProvider creates a new LM Studio provider
|
||||
func NewLMStudioProvider(baseURL, model string, verbose bool) *LMStudioProvider {
|
||||
return &LMStudioProvider{
|
||||
baseURL: baseURL,
|
||||
model: model,
|
||||
client: &http.Client{},
|
||||
verbose: verbose,
|
||||
}
|
||||
}
|
||||
|
||||
type lmStudioMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
ToolCalls []lmStudioToolCall `json:"tool_calls,omitempty"`
|
||||
}
|
||||
|
||||
type lmStudioToolCall struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Function struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
} `json:"function"`
|
||||
}
|
||||
|
||||
type lmStudioRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []lmStudioMessage `json:"messages"`
|
||||
Tools []domain.Tool `json:"tools,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
}
|
||||
|
||||
type lmStudioResponse struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
ToolCalls []lmStudioToolCall `json:"tool_calls,omitempty"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
Error json.RawMessage `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Complete sends a request to LM Studio with function calling support
|
||||
func (p *LMStudioProvider) Complete(ctx context.Context, request domain.LLMRequest) (*domain.LLMResponse, error) {
|
||||
// Convert domain messages to LM Studio format
|
||||
messages := make([]lmStudioMessage, len(request.Messages))
|
||||
for i, msg := range request.Messages {
|
||||
// Convert tool calls if present
|
||||
var toolCalls []lmStudioToolCall
|
||||
if len(msg.ToolCalls) > 0 {
|
||||
toolCalls = make([]lmStudioToolCall, len(msg.ToolCalls))
|
||||
for j, tc := range msg.ToolCalls {
|
||||
toolCalls[j] = lmStudioToolCall{
|
||||
ID: tc.ID,
|
||||
Type: tc.Type,
|
||||
Function: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
}{
|
||||
Name: tc.Function.Name,
|
||||
Arguments: tc.Function.Arguments,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messages[i] = lmStudioMessage{
|
||||
Role: msg.Role,
|
||||
Content: msg.Content,
|
||||
ToolCallID: msg.ToolCallID,
|
||||
ToolCalls: toolCalls,
|
||||
}
|
||||
}
|
||||
|
||||
reqBody := lmStudioRequest{
|
||||
Model: p.model,
|
||||
Messages: messages,
|
||||
Tools: request.Tools,
|
||||
Temperature: 0.7,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshaling request: %w", err)
|
||||
}
|
||||
|
||||
if p.verbose {
|
||||
var prettyJSON bytes.Buffer
|
||||
if err := json.Indent(&prettyJSON, jsonData, "", " "); err == nil {
|
||||
log.Printf("\n========== LM STUDIO REQUEST ==========\nURL: %s/v1/chat/completions\nBody:\n%s\n=======================================\n", p.baseURL, prettyJSON.String())
|
||||
}
|
||||
}
|
||||
|
||||
url := p.baseURL + "/v1/chat/completions"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sending request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
|
||||
if p.verbose {
|
||||
var prettyJSON bytes.Buffer
|
||||
if err := json.Indent(&prettyJSON, body, "", " "); err == nil {
|
||||
log.Printf("\n========== LM STUDIO RESPONSE ==========\nStatus: %d\nBody:\n%s\n========================================\n", resp.StatusCode, prettyJSON.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Check HTTP status
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var apiResp lmStudioResponse
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling response: %w\nResponse body: %s", err, string(body))
|
||||
}
|
||||
|
||||
// Check for error in response
|
||||
if len(apiResp.Error) > 0 {
|
||||
var errStr string
|
||||
if err := json.Unmarshal(apiResp.Error, &errStr); err == nil {
|
||||
return nil, fmt.Errorf("API error: %s", errStr)
|
||||
}
|
||||
var errObj struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(apiResp.Error, &errObj); err == nil {
|
||||
return nil, fmt.Errorf("API error: %s", errObj.Message)
|
||||
}
|
||||
return nil, fmt.Errorf("API error: %s", string(apiResp.Error))
|
||||
}
|
||||
|
||||
if len(apiResp.Choices) == 0 {
|
||||
return nil, fmt.Errorf("no choices in response")
|
||||
}
|
||||
|
||||
// Convert tool calls to domain format
|
||||
toolCalls := make([]domain.ToolCall, len(apiResp.Choices[0].Message.ToolCalls))
|
||||
for i, tc := range apiResp.Choices[0].Message.ToolCalls {
|
||||
toolCalls[i] = domain.ToolCall{
|
||||
ID: tc.ID,
|
||||
Type: tc.Type,
|
||||
Function: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
}{
|
||||
Name: tc.Function.Name,
|
||||
Arguments: tc.Function.Arguments,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &domain.LLMResponse{
|
||||
Content: apiResp.Choices[0].Message.Content,
|
||||
ToolCalls: toolCalls,
|
||||
}, nil
|
||||
}
|
||||
178
internal/infrastructure/llm/openrouter.go
Normal file
178
internal/infrastructure/llm/openrouter.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/paramah/ai_devs4/s01e03/internal/domain"
|
||||
)
|
||||
|
||||
// OpenRouterProvider implements domain.LLMProvider for OpenRouter API
|
||||
type OpenRouterProvider struct {
|
||||
apiKey string
|
||||
model string
|
||||
baseURL string
|
||||
client *http.Client
|
||||
verbose bool
|
||||
}
|
||||
|
||||
// NewOpenRouterProvider creates a new OpenRouter provider
|
||||
func NewOpenRouterProvider(apiKey, model string, verbose bool) *OpenRouterProvider {
|
||||
return &OpenRouterProvider{
|
||||
apiKey: apiKey,
|
||||
model: model,
|
||||
baseURL: "https://openrouter.ai/api/v1/chat/completions",
|
||||
client: &http.Client{},
|
||||
verbose: verbose,
|
||||
}
|
||||
}
|
||||
|
||||
type openRouterMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
ToolCalls []openRouterToolCall `json:"tool_calls,omitempty"`
|
||||
}
|
||||
|
||||
type openRouterToolCall struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Function struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
} `json:"function"`
|
||||
}
|
||||
|
||||
type openRouterRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []openRouterMessage `json:"messages"`
|
||||
Tools []domain.Tool `json:"tools,omitempty"`
|
||||
}
|
||||
|
||||
type openRouterResponse struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
ToolCalls []openRouterToolCall `json:"tool_calls,omitempty"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
Error *struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Complete sends a request to OpenRouter API with function calling support
|
||||
func (p *OpenRouterProvider) Complete(ctx context.Context, request domain.LLMRequest) (*domain.LLMResponse, error) {
|
||||
// Convert domain messages to OpenRouter format
|
||||
messages := make([]openRouterMessage, len(request.Messages))
|
||||
for i, msg := range request.Messages {
|
||||
// Convert tool calls if present
|
||||
var toolCalls []openRouterToolCall
|
||||
if len(msg.ToolCalls) > 0 {
|
||||
toolCalls = make([]openRouterToolCall, len(msg.ToolCalls))
|
||||
for j, tc := range msg.ToolCalls {
|
||||
toolCalls[j] = openRouterToolCall{
|
||||
ID: tc.ID,
|
||||
Type: tc.Type,
|
||||
Function: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
}{
|
||||
Name: tc.Function.Name,
|
||||
Arguments: tc.Function.Arguments,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messages[i] = openRouterMessage{
|
||||
Role: msg.Role,
|
||||
Content: msg.Content,
|
||||
ToolCallID: msg.ToolCallID,
|
||||
ToolCalls: toolCalls,
|
||||
}
|
||||
}
|
||||
|
||||
reqBody := openRouterRequest{
|
||||
Model: p.model,
|
||||
Messages: messages,
|
||||
Tools: request.Tools,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshaling request: %w", err)
|
||||
}
|
||||
|
||||
if p.verbose {
|
||||
var prettyJSON bytes.Buffer
|
||||
if err := json.Indent(&prettyJSON, jsonData, "", " "); err == nil {
|
||||
log.Printf("\n========== LLM REQUEST ==========\nURL: %s\nBody:\n%s\n================================\n", p.baseURL, prettyJSON.String())
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.baseURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+p.apiKey)
|
||||
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sending request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
|
||||
if p.verbose {
|
||||
var prettyJSON bytes.Buffer
|
||||
if err := json.Indent(&prettyJSON, body, "", " "); err == nil {
|
||||
log.Printf("\n========== LLM RESPONSE ==========\nStatus: %d\nBody:\n%s\n==================================\n", resp.StatusCode, prettyJSON.String())
|
||||
}
|
||||
}
|
||||
|
||||
var apiResp openRouterResponse
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling response: %w", err)
|
||||
}
|
||||
|
||||
if apiResp.Error != nil {
|
||||
return nil, fmt.Errorf("API error: %s", apiResp.Error.Message)
|
||||
}
|
||||
|
||||
if len(apiResp.Choices) == 0 {
|
||||
return nil, fmt.Errorf("no choices in response")
|
||||
}
|
||||
|
||||
// Convert tool calls to domain format
|
||||
toolCalls := make([]domain.ToolCall, len(apiResp.Choices[0].Message.ToolCalls))
|
||||
for i, tc := range apiResp.Choices[0].Message.ToolCalls {
|
||||
toolCalls[i] = domain.ToolCall{
|
||||
ID: tc.ID,
|
||||
Type: tc.Type,
|
||||
Function: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
}{
|
||||
Name: tc.Function.Name,
|
||||
Arguments: tc.Function.Arguments,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &domain.LLMResponse{
|
||||
Content: apiResp.Choices[0].Message.Content,
|
||||
ToolCalls: toolCalls,
|
||||
}, nil
|
||||
}
|
||||
184
internal/infrastructure/packages/api_client.go
Normal file
184
internal/infrastructure/packages/api_client.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/paramah/ai_devs4/s01e03/internal/domain"
|
||||
)
|
||||
|
||||
// APIClient implements domain.PackageClient
|
||||
type APIClient struct {
|
||||
baseURL string
|
||||
apiKey string
|
||||
client *http.Client
|
||||
verbose bool
|
||||
}
|
||||
|
||||
// NewAPIClient creates a new package API client
|
||||
func NewAPIClient(baseURL, apiKey string, verbose bool) *APIClient {
|
||||
return &APIClient{
|
||||
baseURL: baseURL,
|
||||
apiKey: apiKey,
|
||||
client: &http.Client{},
|
||||
verbose: verbose,
|
||||
}
|
||||
}
|
||||
|
||||
type checkRequest struct {
|
||||
APIKey string `json:"apikey"`
|
||||
Action string `json:"action"`
|
||||
PackageID string `json:"packageid"`
|
||||
}
|
||||
|
||||
type checkResponse struct {
|
||||
Status string `json:"status"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type redirectRequest struct {
|
||||
APIKey string `json:"apikey"`
|
||||
Action string `json:"action"`
|
||||
PackageID string `json:"packageid"`
|
||||
Destination string `json:"destination"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
type redirectResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Confirmation string `json:"confirmation,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Check checks the status of a package
|
||||
func (c *APIClient) Check(ctx context.Context, packageID string) (*domain.PackageStatus, error) {
|
||||
reqBody := checkRequest{
|
||||
APIKey: c.apiKey,
|
||||
Action: "check",
|
||||
PackageID: packageID,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshaling request: %w", err)
|
||||
}
|
||||
|
||||
if c.verbose {
|
||||
var prettyJSON bytes.Buffer
|
||||
if err := json.Indent(&prettyJSON, jsonData, "", " "); err == nil {
|
||||
log.Printf("\n========== PACKAGE API CHECK REQUEST ==========\nURL: %s\nBody:\n%s\n===============================================\n", c.baseURL, prettyJSON.String())
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sending request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
|
||||
if c.verbose {
|
||||
var prettyJSON bytes.Buffer
|
||||
if err := json.Indent(&prettyJSON, body, "", " "); err == nil {
|
||||
log.Printf("\n========== PACKAGE API CHECK RESPONSE ==========\nStatus: %d\nBody:\n%s\n================================================\n", resp.StatusCode, prettyJSON.String())
|
||||
}
|
||||
}
|
||||
|
||||
var apiResp checkResponse
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling response: %w", err)
|
||||
}
|
||||
|
||||
if apiResp.Error != "" {
|
||||
return nil, fmt.Errorf("API error: %s", apiResp.Error)
|
||||
}
|
||||
|
||||
return &domain.PackageStatus{
|
||||
PackageID: packageID,
|
||||
Status: apiResp.Status,
|
||||
Location: apiResp.Location,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Redirect redirects a package to a new destination and returns the confirmation message
|
||||
func (c *APIClient) Redirect(ctx context.Context, packageID, destination, code string) (string, error) {
|
||||
reqBody := redirectRequest{
|
||||
APIKey: c.apiKey,
|
||||
Action: "redirect",
|
||||
PackageID: packageID,
|
||||
Destination: destination,
|
||||
Code: code,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshaling request: %w", err)
|
||||
}
|
||||
|
||||
if c.verbose {
|
||||
var prettyJSON bytes.Buffer
|
||||
if err := json.Indent(&prettyJSON, jsonData, "", " "); err == nil {
|
||||
log.Printf("\n========== PACKAGE API REDIRECT REQUEST ==========\nURL: %s\nBody:\n%s\n==================================================\n", c.baseURL, prettyJSON.String())
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("sending request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
|
||||
if c.verbose {
|
||||
var prettyJSON bytes.Buffer
|
||||
if err := json.Indent(&prettyJSON, body, "", " "); err == nil {
|
||||
log.Printf("\n========== PACKAGE API REDIRECT RESPONSE ==========\nStatus: %d\nBody:\n%s\n===================================================\n", resp.StatusCode, prettyJSON.String())
|
||||
}
|
||||
}
|
||||
|
||||
var apiResp redirectResponse
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
return "", fmt.Errorf("unmarshaling response: %w", err)
|
||||
}
|
||||
|
||||
if apiResp.Error != "" {
|
||||
return "", fmt.Errorf("API error: %s", apiResp.Error)
|
||||
}
|
||||
|
||||
// Return the confirmation code
|
||||
if apiResp.Confirmation != "" {
|
||||
return apiResp.Confirmation, nil
|
||||
}
|
||||
|
||||
// Fallback to message if confirmation is not present
|
||||
return apiResp.Message, nil
|
||||
}
|
||||
74
internal/infrastructure/session/file_storage.go
Normal file
74
internal/infrastructure/session/file_storage.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/paramah/ai_devs4/s01e03/internal/domain"
|
||||
)
|
||||
|
||||
// FileStorage implements domain.SessionStorage using JSON files
|
||||
type FileStorage struct {
|
||||
baseDir string
|
||||
}
|
||||
|
||||
// NewFileStorage creates a new file-based session storage
|
||||
func NewFileStorage(baseDir string) *FileStorage {
|
||||
return &FileStorage{
|
||||
baseDir: baseDir,
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves a session from file storage
|
||||
func (s *FileStorage) Get(ctx context.Context, sessionID string) (*domain.Session, error) {
|
||||
filePath := s.getFilePath(sessionID)
|
||||
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
// Return empty session if file doesn't exist
|
||||
return &domain.Session{
|
||||
ID: sessionID,
|
||||
Messages: []domain.Message{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading session file: %w", err)
|
||||
}
|
||||
|
||||
var session domain.Session
|
||||
if err := json.Unmarshal(data, &session); err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling session: %w", err)
|
||||
}
|
||||
|
||||
return &session, nil
|
||||
}
|
||||
|
||||
// Save saves a session to file storage
|
||||
func (s *FileStorage) Save(ctx context.Context, session *domain.Session) error {
|
||||
filePath := s.getFilePath(session.ID)
|
||||
|
||||
// Ensure directory exists
|
||||
if err := os.MkdirAll(s.baseDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating cache directory: %w", err)
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(session, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling session: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filePath, data, 0644); err != nil {
|
||||
return fmt.Errorf("writing session file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FileStorage) getFilePath(sessionID string) string {
|
||||
return filepath.Join(s.baseDir, fmt.Sprintf("%s.json", sessionID))
|
||||
}
|
||||
127
internal/infrastructure/verify/client.go
Normal file
127
internal/infrastructure/verify/client.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package verify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/big"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Client handles verification with the hub
|
||||
type Client struct {
|
||||
verifyURL string
|
||||
apiKey string
|
||||
client *http.Client
|
||||
verbose bool
|
||||
}
|
||||
|
||||
// NewClient creates a new verify client
|
||||
func NewClient(verifyURL, apiKey string, verbose bool) *Client {
|
||||
return &Client{
|
||||
verifyURL: verifyURL,
|
||||
apiKey: apiKey,
|
||||
client: &http.Client{},
|
||||
verbose: verbose,
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyRequest represents the request to the verify endpoint
|
||||
type VerifyRequest struct {
|
||||
APIKey string `json:"apikey"`
|
||||
Task string `json:"task"`
|
||||
Answer VerifyAnswer `json:"answer"`
|
||||
}
|
||||
|
||||
// VerifyAnswer represents the answer part of verify request
|
||||
type VerifyAnswer struct {
|
||||
URL string `json:"url"`
|
||||
SessionID string `json:"sessionID"`
|
||||
}
|
||||
|
||||
// VerifyResponse represents the response from verify endpoint
|
||||
type VerifyResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Register registers the proxy with the hub
|
||||
func (c *Client) Register(ctx context.Context, tunnelURL, sessionID string) error {
|
||||
reqBody := VerifyRequest{
|
||||
APIKey: c.apiKey,
|
||||
Task: "proxy",
|
||||
Answer: VerifyAnswer{
|
||||
URL: tunnelURL,
|
||||
SessionID: sessionID,
|
||||
},
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling request: %w", err)
|
||||
}
|
||||
|
||||
if c.verbose {
|
||||
var prettyJSON bytes.Buffer
|
||||
if err := json.Indent(&prettyJSON, jsonData, "", " "); err == nil {
|
||||
log.Printf("\n========== VERIFY REQUEST ==========\nURL: %s\nBody:\n%s\n====================================\n", c.verifyURL, prettyJSON.String())
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.verifyURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sending request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
|
||||
if c.verbose {
|
||||
var prettyJSON bytes.Buffer
|
||||
if err := json.Indent(&prettyJSON, body, "", " "); err == nil {
|
||||
log.Printf("\n========== VERIFY RESPONSE ==========\nStatus: %d\nBody:\n%s\n=====================================\n", resp.StatusCode, prettyJSON.String())
|
||||
}
|
||||
}
|
||||
|
||||
var apiResp VerifyResponse
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
return fmt.Errorf("unmarshaling response: %w (body: %s)", err, string(body))
|
||||
}
|
||||
|
||||
if apiResp.Error != "" {
|
||||
return fmt.Errorf("API error: %s", apiResp.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateSessionID generates a random alphanumeric session ID
|
||||
func GenerateSessionID(length int) (string, error) {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
result := make([]byte, length)
|
||||
|
||||
for i := range result {
|
||||
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("generating random number: %w", err)
|
||||
}
|
||||
result[i] = charset[num.Int64()]
|
||||
}
|
||||
|
||||
return string(result), nil
|
||||
}
|
||||
125
internal/infrastructure/weather/api_client.go
Normal file
125
internal/infrastructure/weather/api_client.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package weather
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/paramah/ai_devs4/s01e03/internal/domain"
|
||||
)
|
||||
|
||||
// APIClient implements domain.WeatherClient using wttr.in
|
||||
type APIClient struct {
|
||||
baseURL string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewAPIClient creates a new weather API client
|
||||
// apiKey parameter is ignored as wttr.in doesn't require authentication
|
||||
func NewAPIClient(baseURL, apiKey string) *APIClient {
|
||||
if baseURL == "" {
|
||||
baseURL = "https://wttr.in"
|
||||
}
|
||||
return &APIClient{
|
||||
baseURL: baseURL,
|
||||
client: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
type wttrResponse struct {
|
||||
CurrentCondition []struct {
|
||||
TempC string `json:"temp_C"`
|
||||
Humidity string `json:"humidity"`
|
||||
WeatherDesc []struct {
|
||||
Value string `json:"value"`
|
||||
} `json:"weatherDesc"`
|
||||
} `json:"current_condition"`
|
||||
NearestArea []struct {
|
||||
AreaName []struct {
|
||||
Value string `json:"value"`
|
||||
} `json:"areaName"`
|
||||
} `json:"nearest_area"`
|
||||
}
|
||||
|
||||
// GetWeather gets weather information for a location using wttr.in
|
||||
func (c *APIClient) GetWeather(ctx context.Context, location string) (*domain.WeatherInfo, error) {
|
||||
// Build URL - wttr.in format: https://wttr.in/Location?format=j1
|
||||
u, err := url.Parse(c.baseURL + "/" + url.PathEscape(location))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing URL: %w", err)
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
q.Set("format", "j1")
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
||||
// wttr.in prefers this header to return JSON
|
||||
req.Header.Set("User-Agent", "curl/7.0")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sending request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
|
||||
var apiResp wttrResponse
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling response: %w", err)
|
||||
}
|
||||
|
||||
if len(apiResp.CurrentCondition) == 0 {
|
||||
return nil, fmt.Errorf("no weather data available for location: %s", location)
|
||||
}
|
||||
|
||||
current := apiResp.CurrentCondition[0]
|
||||
|
||||
// Parse temperature
|
||||
temp, err := strconv.ParseFloat(current.TempC, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing temperature: %w", err)
|
||||
}
|
||||
|
||||
// Parse humidity
|
||||
humidity, err := strconv.Atoi(current.Humidity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing humidity: %w", err)
|
||||
}
|
||||
|
||||
// Get description
|
||||
description := "unknown"
|
||||
if len(current.WeatherDesc) > 0 {
|
||||
description = current.WeatherDesc[0].Value
|
||||
}
|
||||
|
||||
// Get location name
|
||||
locationName := location
|
||||
if len(apiResp.NearestArea) > 0 && len(apiResp.NearestArea[0].AreaName) > 0 {
|
||||
locationName = apiResp.NearestArea[0].AreaName[0].Value
|
||||
}
|
||||
|
||||
return &domain.WeatherInfo{
|
||||
Location: locationName,
|
||||
Temperature: temp,
|
||||
Description: description,
|
||||
Humidity: humidity,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user