This commit is contained in:
2026-03-12 02:10:57 +01:00
parent a8e769c73d
commit 3c6dd52d8f
29 changed files with 3323 additions and 0 deletions

22
.gitignore vendored Normal file
View File

@@ -0,0 +1,22 @@
# Binaries
person-processor
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool
*.out
# Go workspace file
go.work
# Config with secrets
config.json
# Output directories
output/

19
CLAUDE.md Normal file
View File

@@ -0,0 +1,19 @@
Potrzebuję aplikacji, która napisana w golang oparta o clean architecture. Będziemy koszystali z LLM, wykorzystaj sposób łączenia z llm taki sam jak w projekcie który jest dostępny w katalogu ../s01e01/ przenieś potrzebne elementy z tamtej aplikacji. Nasza aplikacja zrealizuje zadanie:
1. w pliku lista.json mamy dane osób, które musimy pobrać do dalszej pracy
2. Wysyłamy na endpoint za pomocą POST w raw json zapytanie dla każdej z osób na liście w takiej formie
{
"apikey": "tutaj-twój-klucz",
"name": "Jan",
"surname": "Kowalski"
}
3. odpowiedzi zapisujemy w plikach json w katalogu outout/locations/
4. na endpoint https://hub.ag3nts.org/api/accesslevel metodą POST w raw json wysyłamy zapytanie dla każdej z osób w formie
{
"apikey": "tutaj-twój-klucz",
"name": "Jan",
"surname": "Kowalski",
"birthYear": 1987
}
4. odpowiedzi zapisujemy w plikach json w katalogu output/accesslevel

365
README.md Normal file
View File

@@ -0,0 +1,365 @@
# AI Agent Person Processor ⚡
Zaawansowana aplikacja w Go oparta o Clean Architecture, która wykorzystuje **ZOPTYMALIZOWANY** proces z LLM Function Calling + równoległymi obliczeniami do inteligentnego przetwarzania danych osób i generowania raportów lokalizacyjnych.
## 🚀 Kluczowa optymalizacja:
- **Tylko 2 wywołania LLM na osobę** (poprzednio: 30-50)
- **Równoległe obliczenia odległości** (wszystkie elektrownie jednocześnie)
- **20x szybsze** przetwarzanie
- **Wielowątkowe goroutines** w Go
## Funkcjonalność
### Zoptymalizowany proces przetwarzania:
**Dla każdej osoby sekwencyjnie - TYLKO 2 WYWOŁANIA LLM:**
1. **[LLM Call 1/2] Pobierz lokalizacje osoby** (function calling: `get_location`)
- Zwraca **WSZYSTKIE** możliwe lokalizacje osoby (np. 7, 12, 17 lokalizacji)
2. **[Lokalne obliczenia RÓWNOLEGŁE] Znajdź globalnie najbliższą elektrownię**
- **Dla KAŻDEJ lokalizacji osoby** (pętla):
- Dla KAŻDEJ elektrowni uruchamia oddzielną goroutine
- Każda goroutine pobiera współrzędne z geocoding cache
- Równolegle oblicza odległość Haversine (bez LLM!)
- Znajduje najbliższą elektrownię dla tej konkretnej lokalizacji
- **Wybiera MINIMUM ze wszystkich lokalizacji×elektrowni**
- **Wielowątkowe przetwarzanie** - wszystkie elektrownie jednocześnie
3. **[LLM Call 2/2] Pobierz poziom dostępu** (function calling: `get_access_level`)
- Używa tylko roku urodzenia (integer)
4. **Generuje raport dla osoby**
- Zapisuje w `output/person_reports/{Name}_{Surname}.json`
- Zawiera: nazwę elektrowni, kod, odległość, poziom dostępu, współrzędne
**Po przetworzeniu wszystkich osób:**
- Znajduje osobę z **najmniejszą odległością** do jej najbliższej elektrowni
- Generuje `output/final_answer.json` do weryfikacji
## Architektura
```
.
├── cmd/app/ # Punkt wejścia
├── internal/
│ ├── config/ # Konfiguracja
│ ├── domain/ # Modele i interfejsy
│ │ ├── person.go # Model osoby
│ │ ├── location.go # Haversine i obliczenia odległości
│ │ ├── report.go # Modele raportów
│ │ ├── llm.go # LLM z function calling
│ │ └── tools.go # Definicje narzędzi dla agenta
│ ├── infrastructure/
│ │ ├── api/ # Klient API hub.ag3nts.org
│ │ ├── json/ # Repozytoria JSON
│ │ └── llm/ # Providery LLM
│ └── usecase/
│ └── person_agent_processor.go # Logika przetwarzania osób przez agenta
├── output/
│ ├── locations/ # Dane lokalizacji osób (z API)
│ ├── accesslevel/ # Dane poziomów dostępu (z API)
│ ├── person_reports/ # Raport dla każdej osoby
│ ├── findhim_locations.json # Lista elektrowni z kodami
│ └── final_answer.json # Końcowa odpowiedź do weryfikacji
└── lista.json # Dane wejściowe
```
## Przykładowy raport osoby
Raport dla osoby (`output/person_reports/Oskar_Sieradzki.json`):
```json
{
"name": "Oskar",
"surname": "Sieradzki",
"nearest_plant": "Grudziądz",
"plant_code": "PWR7264PL",
"distance_km": 83.38,
"access_level": 7,
"primary_latitude": 53.483,
"primary_longitude": 18.754
}
```
Każdy raport zawiera najbliższą elektrownię dla danej osoby wraz z odległością i kodem elektrowni.
## Optymalizacje
### ⚡ EKSTREMALNA OPTYMALIZACJA - 2 WYWOŁANIA LLM NA OSOBĘ:
**Poprzednia wersja:** 30-50 iteracji LLM na osobę
**Nowa wersja:** **TYLKO 2 wywołania LLM na osobę**
### Jak to działa:
**FAZA INICJALIZACJI (jednokrotnie przy starcie):**
- Pobiera listę elektrowni z API
- **Geocoding API (OpenStreetMap Nominatim):** dla każdej elektrowni pobiera dokładne współrzędne
- Zapisuje współrzędne w pamięci cache
- Fallback do `CityCoordinates` jeśli API nie odpowiada
**FAZA PRZETWARZANIA (dla każdej osoby):**
1. **LLM Call 1:** `get_location` - pobiera **WSZYSTKIE** lokalizacje osoby (function calling)
2. **Lokalne obliczenia (RÓWNOLEGŁE dla WSZYSTKICH lokalizacji):**
- **Dla KAŻDEJ lokalizacji osoby:**
- Dla każdej elektrowni: osobna goroutine
- Pobiera współrzędne z cache (dokładne z geocoding API!)
- Oblicza odległość Haversine
- Znajduje najbliższą elektrownię dla tej lokalizacji
- **Wybiera GLOBALNIE najmniejszą odległość** ze wszystkich kombinacji
- Wszystko dzieje się **jednocześnie** (wielowątkowo)
- **BEZ użycia LLM!**
3. **LLM Call 2:** `get_access_level` - pobiera poziom dostępu (function calling)
### Kluczowe ulepszenia:
- **Redukcja wywołań LLM z ~40 do 2** (20x szybciej! 💨)
- **Równoległe przetwarzanie** - wszystkie elektrownie jednocześnie
- **Dokładne współrzędne** - OpenStreetMap Nominatim API (geocoding)
- **Geocoding przy starcie** - jednokrotne pobranie współrzędnych wszystkich elektrowni
- **Haversine bez LLM** - lokalne obliczenia w Go
- **Fallback coordinates** - jeśli geocoding API zawiedzie, używamy CityCoordinates
- Temperature = 0.0 dla deterministycznych wyników
### Zalety tego podejścia:
-**Dramatycznie szybsze** - 20x mniej wywołań LLM
- 💰 **Tańsze** - minimalna konsumpcja API
- 🚀 **Równoległe obliczenia** - wykorzystanie wszystkich rdzeni CPU
- 🎯 **Deterministyczne** - te same wyniki za każdym razem
- 📊 **Szczegółowe logi** - każdy krok widoczny
- 🔧 **Łatwe debugowanie** - jasna struktura 2-fazowa
### System logowania:
Aplikacja generuje bardzo szczegółowe logi pokazujące:
- **Fazę ładowania danych** (osoby, elektrownie)
- **Dla każdej osoby:**
- `[LLM Call 1/2]` - wywołanie get_location
- `[Local Processing]` - równoległe goroutines dla każdej elektrowni
- Wyniki z każdej goroutine: `Goroutine [Zabrze]: 123.45 km`
- `[LLM Call 2/2]` - wywołanie get_access_level
- Finalne wyniki z podsumowaniem `Total LLM calls: 2`
- **Fazę znajdowania minimalnej odległości**
- **Ostateczny wynik** z instrukcją weryfikacji
## Konfiguracja
```json
{
"api_key": "your-api-key",
"input_file": "lista.json",
"output_dir": "output",
"locations_api": "https://hub.ag3nts.org/api/location",
"access_level_api": "https://hub.ag3nts.org/api/accesslevel",
"locations_url": "https://hub.ag3nts.org",
"llm": {
"provider": "lmstudio",
"model": "bielik-11b-v3.0-instruct-gptq-marlin@q8_0",
"base_url": "http://localhost:1234"
}
}
```
### Providery LLM
#### LM Studio (lokalny)
```json
"llm": {
"provider": "lmstudio",
"model": "bielik-11b-v3.0-instruct-gptq-marlin@q8_0",
"base_url": "http://localhost:1234"
}
```
#### OpenRouter (API)
```json
"llm": {
"provider": "openrouter",
"model": "anthropic/claude-3.5-sonnet",
"api_key": "your-openrouter-api-key"
}
```
**Uwaga**: Model Bielik jest zalecany dla function calling - działa szybciej i stabilniej niż DeepSeek R1.
## Budowanie i uruchomienie
```bash
# Budowanie
go build -o person-processor ./cmd/app
# Uruchomienie
./person-processor -config config.json
```
## Weryfikacja odpowiedzi
Po zakończeniu przetwarzania, aplikacja zapisuje końcową odpowiedź w pliku `output/final_answer.json`.
Aby wysłać odpowiedź do weryfikacji:
```bash
curl -X POST https://hub.ag3nts.org/verify \
-H "Content-Type: application/json" \
-d @output/final_answer.json
```
Lub z formatowaniem odpowiedzi:
```bash
curl -X POST https://hub.ag3nts.org/verify \
-H "Content-Type: application/json" \
-d @output/final_answer.json | jq .
```
Format odpowiedzi (`output/final_answer.json`):
```json
{
"apikey": "your-api-key",
"task": "findhim",
"answer": {
"name": "Imię",
"surname": "Nazwisko",
"accessLevel": 7,
"powerPlant": "PWR1234PL"
}
}
```
## Wzór Haversine
Aplikacja używa wzoru Haversine do obliczania odległości między dwoma punktami na kuli ziemskiej:
```
a = sin²(Δlat/2) + cos(lat1) × cos(lat2) × sin²(Δlon/2)
c = 2 × atan2(√a, √(1a))
distance = R × c
```
gdzie R = 6371 km (promień Ziemi)
## Narzędzia agenta LLM i lokalne funkcje
### 🤖 LLM Function Calling (2 wywołania na osobę):
#### get_location
Pobiera wszystkie możliwe lokalizacje osoby z API.
**Parametry:**
- `name` (string) - imię osoby
- `surname` (string) - nazwisko osoby
**Zwraca:** tablicę obiektów z `latitude` i `longitude`
**Logi:** `→ API call: get_location(Imię, Nazwisko)`
**Użycie:** LLM Call 1/2
#### get_access_level
Pobiera poziom dostępu osoby z API.
**Parametry:**
- `name` (string) - imię osoby
- `surname` (string) - nazwisko osoby
- `birth_year` (integer) - **tylko rok** urodzenia (np. 1987, NIE pełna data)
**Zwraca:** obiekt z `accessLevel` (integer)
**Logi:** `→ API call: get_access_level(Imię, Nazwisko, rok)`
**Użycie:** LLM Call 2/2
#### find_nearest_point
Znajduje najbliższy punkt z listy do punktu referencyjnego (np. elektrownia).
**Parametry:**
- `reference_lat` (float) - szerokość geograficzna punktu referencyjnego
- `reference_lon` (float) - długość geograficzna punktu referencyjnego
- `points` (array) - tablica punktów do sprawdzenia, każdy punkt: `[latitude, longitude]`
**Zwraca:** obiekt z `latitude`, `longitude`, `distance_km`, `index` (indeks najbliższego punktu)
**Przykład wywołania:**
```json
{
"reference_lat": 50.3086,
"reference_lon": 18.7864,
"points": [
[52.2297, 21.0122],
[51.1079, 17.0385],
[50.0647, 19.9450]
]
}
```
**Zwraca:**
```json
{
"latitude": 50.0647,
"longitude": 19.9450,
"distance_km": 45.23,
"index": 2
}
```
**Logi:** `→ Tool call: find_nearest_point(ref: 50.3086,18.7864, 3 points)`
**Użycie:** Opcjonalna - dostępna dla LLM jeśli zdecyduje się jej użyć
---
### ⚡ Lokalne funkcje (bez LLM - wielowątkowe):
#### GetPlantGeolocation (Geocoding API)
Pobiera dokładne współrzędne geograficzne elektrowni z OpenStreetMap Nominatim API.
**API:** `https://nominatim.openstreetmap.org/search`
**Parametry:**
- `city` - nazwa miasta
- `country` - Poland
- `format` - json
- `limit` - 1
**Zwraca:** Dokładne współrzędne `{lat, lon}` (6 miejsc po przecinku!)
**Użycie:** Jednokrotnie przy starcie aplikacji (cache w pamięci)
**Logi:**
```
→ Fetching accurate geolocations from geocoding API...
• Geocoding: Zabrze...
✓ Fetched: 50.308615°N, 18.786375°E
• Geocoding: Grudziądz...
✓ Fetched: 53.483624°N, 18.753536°E
```
#### CityCoordinates (baza fallback)
Hardcoded współrzędne wszystkich elektrowni w Polsce (fallback gdy API nie odpowiada).
**Zawartość:**
- Zabrze, Piotrków Trybunalski, Grudziądz, Tczew, Radom, Chełmno, Żarnowiec
- Każde miasto: `{Lat: float64, Lon: float64}`
**Użycie:** Fallback gdy geocoding API zawiedzie
#### Haversine (wzór odległości)
Oblicza odległość między dwoma punktami na kuli ziemskiej.
**Parametry:**
- `lat1, lon1` - współrzędne punktu 1
- `lat2, lon2` - współrzędne punktu 2
**Zwraca:** odległość w kilometrach (float64)
**Użycie:** Równoległe goroutines dla każdej elektrowni (bez LLM!)
**Logi:** `• Goroutine [Nazwa]: 123.45 km`
## Wymagania
- Go 1.21+
- LM Studio lub klucz OpenRouter API
- Model LLM wspierający function calling (zalecany: Bielik 11B)

73
cmd/app/main.go Normal file
View File

@@ -0,0 +1,73 @@
package main
import (
"context"
"flag"
"log"
"time"
"github.com/paramah/ai_devs4/s01e02/internal/config"
"github.com/paramah/ai_devs4/s01e02/internal/domain"
"github.com/paramah/ai_devs4/s01e02/internal/infrastructure/api"
"github.com/paramah/ai_devs4/s01e02/internal/infrastructure/json"
"github.com/paramah/ai_devs4/s01e02/internal/infrastructure/llm"
"github.com/paramah/ai_devs4/s01e02/internal/usecase"
)
func main() {
configPath := flag.String("config", "config.json", "Path to configuration file")
flag.Parse()
// Load configuration
cfg, err := config.Load(*configPath)
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}
if err := cfg.Validate(); err != nil {
log.Fatalf("Invalid configuration: %v", err)
}
log.Printf("========== AI Agent Person Processor ==========")
log.Printf("Input file: %s", cfg.InputFile)
log.Printf("Output directory: %s", cfg.OutputDir)
log.Printf("LLM Provider: %s", cfg.LLM.Provider)
log.Printf("LLM Model: %s", cfg.LLM.Model)
log.Printf("===============================================\n")
// Create repositories and clients
personRepo := json.NewRepository()
apiClient := api.NewClient(cfg.LocationsAPI, cfg.AccessLevelAPI)
// Create LLM provider
var llmProvider domain.LLMProvider
switch cfg.LLM.Provider {
case "openrouter":
llmProvider = llm.NewOpenRouterProvider(cfg.LLM.APIKey, cfg.LLM.Model)
log.Printf("Using OpenRouter with model: %s", cfg.LLM.Model)
case "lmstudio":
llmProvider = llm.NewLMStudioProvider(cfg.LLM.BaseURL, cfg.LLM.Model)
log.Printf("Using LM Studio at %s with model: %s", cfg.LLM.BaseURL, cfg.LLM.Model)
default:
log.Fatalf("Unknown LLM provider: %s", cfg.LLM.Provider)
}
// Create person agent processor use case
personAgentUC := usecase.NewPersonAgentProcessorUseCase(
personRepo,
apiClient,
llmProvider,
cfg.APIKey,
cfg.OutputDir,
)
// Execute processing
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
if err := personAgentUC.Execute(ctx, cfg.InputFile); err != nil {
log.Fatalf("Failed to process persons: %v", err)
}
log.Printf("\n========== All tasks completed successfully! ==========")
}

7
config.example.json Normal file
View File

@@ -0,0 +1,7 @@
{
"api_key": "YOUR_API_KEY_HERE",
"input_file": "lista.json",
"output_dir": "output",
"locations_api": "https://hub.ag3nts.org/api/locations",
"access_level_api": "https://hub.ag3nts.org/api/accesslevel"
}

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/paramah/ai_devs4/s01e02
go 1.25.0

86
internal/config/config.go Normal file
View File

@@ -0,0 +1,86 @@
package config
import (
"encoding/json"
"fmt"
"os"
)
// Config represents the application configuration
type Config struct {
APIKey string `json:"api_key"`
InputFile string `json:"input_file"`
OutputDir string `json:"output_dir"`
LocationsAPI string `json:"locations_api"`
AccessLevelAPI string `json:"access_level_api"`
LocationsURL string `json:"locations_url"`
LLM LLMConfig `json:"llm"`
}
// LLMConfig contains configuration for LLM provider
type LLMConfig struct {
Provider string `json:"provider"` // "openrouter" or "lmstudio"
Model string `json:"model"`
APIKey string `json:"api_key,omitempty"` // For OpenRouter
BaseURL string `json:"base_url,omitempty"` // For LM Studio
}
// Load loads configuration from a JSON file
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading config file: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parsing config file: %w", err)
}
return &cfg, nil
}
// Validate validates the configuration
func (c *Config) Validate() error {
if c.APIKey == "" {
return fmt.Errorf("api_key is required")
}
if c.InputFile == "" {
return fmt.Errorf("input_file is required")
}
if c.OutputDir == "" {
return fmt.Errorf("output_dir is required")
}
if c.LocationsAPI == "" {
return fmt.Errorf("locations_api is required")
}
if c.AccessLevelAPI == "" {
return fmt.Errorf("access_level_api is required")
}
if c.LocationsURL == "" {
return fmt.Errorf("locations_url is required")
}
if c.LLM.Provider != "openrouter" && c.LLM.Provider != "lmstudio" {
return fmt.Errorf("llm.provider must be 'openrouter' or 'lmstudio'")
}
if c.LLM.Model == "" {
return fmt.Errorf("llm.model is required")
}
if c.LLM.Provider == "openrouter" && c.LLM.APIKey == "" {
return fmt.Errorf("llm.api_key is required for openrouter provider")
}
if c.LLM.Provider == "lmstudio" && c.LLM.BaseURL == "" {
return fmt.Errorf("llm.base_url is required for lmstudio provider")
}
return nil
}

View File

@@ -0,0 +1,24 @@
package domain
import "context"
// LocationRequest represents a request for location information
type LocationRequest struct {
APIKey string `json:"apikey"`
Name string `json:"name"`
Surname string `json:"surname"`
}
// AccessLevelRequest represents a request for access level information
type AccessLevelRequest struct {
APIKey string `json:"apikey"`
Name string `json:"name"`
Surname string `json:"surname"`
BirthYear int `json:"birthYear"`
}
// APIClient defines the interface for API operations
type APIClient interface {
GetLocation(ctx context.Context, req LocationRequest) ([]byte, error)
GetAccessLevel(ctx context.Context, req AccessLevelRequest) ([]byte, error)
}

View File

@@ -0,0 +1,50 @@
package domain
// CityCoordinates contains coordinates for Polish cities with power plants
var CityCoordinates = map[string]struct{ Lat, Lon float64 }{
"Zabrze": {Lat: 50.3015, Lon: 18.7912},
"Piotrków Trybunalski": {Lat: 51.4054, Lon: 19.7031},
"Grudziądz": {Lat: 53.4836, Lon: 18.7536},
"Tczew": {Lat: 54.0915, Lon: 18.7793},
"Radom": {Lat: 51.4027, Lon: 21.1471},
"Chelmno": {Lat: 53.3479, Lon: 18.4256},
"Żarnowiec": {Lat: 54.7317, Lon: 18.0431},
}
// PowerPlantInfo contains information about a power plant
type PowerPlantInfo struct {
IsActive bool `json:"is_active"`
Power string `json:"power"`
Code string `json:"code"`
}
// PowerPlantsData represents the structure from findhim_locations.json
type PowerPlantsData struct {
PowerPlants map[string]PowerPlantInfo `json:"power_plants"`
}
// ToLocations converts power plants data to locations with coordinates
func (p *PowerPlantsData) ToLocations() []Location {
var locations []Location
for city, info := range p.PowerPlants {
// Only include active power plants
if !info.IsActive {
continue
}
coords, ok := CityCoordinates[city]
if !ok {
// Skip cities without known coordinates
continue
}
locations = append(locations, Location{
Name: city,
Latitude: coords.Lat,
Longitude: coords.Lon,
})
}
return locations
}

View File

@@ -0,0 +1,66 @@
package domain
// FinalAnswer represents the final answer for the task
type FinalAnswer struct {
APIKey string `json:"apikey"`
Task string `json:"task"`
Answer AnswerDetail `json:"answer"`
}
// AnswerDetail contains the person and power plant details
type AnswerDetail struct {
Name string `json:"name"`
Surname string `json:"surname"`
AccessLevel int `json:"accessLevel"`
PowerPlant string `json:"powerPlant"`
}
// FindClosestPowerPlant finds the power plant closest to any person
// Returns the person, power plant, and distance
func FindClosestPowerPlant(personDataMap map[string]*PersonData, powerPlants []Location, plantsData *PowerPlantsData) (*PersonData, *Location, float64, string) {
var closestPerson *PersonData
var closestPlant *Location
minDistance := 1e10
var powerPlantCode string
for _, personData := range personDataMap {
if len(personData.Locations) == 0 {
continue
}
// Use first location (primary)
personLoc := personData.Locations[0]
for _, plant := range powerPlants {
distance := Haversine(
personLoc.Latitude,
personLoc.Longitude,
plant.Latitude,
plant.Longitude,
)
// If distance is smaller, or same distance but higher access level
if distance < minDistance {
minDistance = distance
closestPerson = personData
closestPlant = &plant
// Get power plant code from plantsData
if info, ok := plantsData.PowerPlants[plant.Name]; ok {
powerPlantCode = info.Code
}
} else if distance == minDistance && closestPerson != nil && personData.AccessLevel > closestPerson.AccessLevel {
// Same distance, but higher access level
closestPerson = personData
closestPlant = &plant
// Get power plant code from plantsData
if info, ok := plantsData.PowerPlants[plant.Name]; ok {
powerPlantCode = info.Code
}
}
}
}
return closestPerson, closestPlant, minDistance, powerPlantCode
}

56
internal/domain/llm.go Normal file
View File

@@ -0,0 +1,56 @@
package domain
import "context"
// LLMMessage represents a message in the conversation
type LLMMessage struct {
Role string `json:"role"`
Content string `json:"content,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
}
// ToolCall represents a function call from the LLM
type ToolCall struct {
ID string `json:"id"`
Type string `json:"type"`
Function Function `json:"function"`
}
// Function represents the function being called
type Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
}
// Tool represents a tool definition for function calling
type Tool struct {
Type string `json:"type"`
Function FunctionDef `json:"function"`
}
// FunctionDef defines a function that can be called
type FunctionDef struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters interface{} `json:"parameters"`
}
// LLMRequest represents a request to the LLM with function calling support
type LLMRequest struct {
Messages []LLMMessage `json:"messages"`
Tools []Tool `json:"tools,omitempty"`
ToolChoice string `json:"tool_choice,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
}
// LLMResponse represents the response from the LLM
type LLMResponse struct {
Message LLMMessage `json:"message"`
FinishReason string `json:"finish_reason"`
}
// LLMProvider defines the interface for LLM providers
type LLMProvider interface {
Chat(ctx context.Context, request LLMRequest) (*LLMResponse, error)
}

180
internal/domain/location.go Normal file
View File

@@ -0,0 +1,180 @@
package domain
import (
"context"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"time"
)
// Location represents a location with coordinates
type Location struct {
Name string `json:"name"`
Latitude float64 `json:"lat"`
Longitude float64 `json:"lon"`
}
// PersonLocation represents a person's location
type PersonLocation struct {
Name string `json:"name"`
Surname string `json:"surname"`
Latitude float64 `json:"lat"`
Longitude float64 `json:"lon"`
}
// DistanceResult represents the distance calculation result
type DistanceResult struct {
Person string `json:"person"`
Location string `json:"location"`
DistanceKm float64 `json:"distance_km"`
}
// Haversine calculates the distance between two points on Earth (in kilometers)
// using the Haversine formula
func Haversine(lat1, lon1, lat2, lon2 float64) float64 {
const earthRadiusKm = 6371.0
// Convert degrees to radians
lat1Rad := lat1 * math.Pi / 180
lat2Rad := lat2 * math.Pi / 180
deltaLat := (lat2 - lat1) * math.Pi / 180
deltaLon := (lon2 - lon1) * math.Pi / 180
// Haversine formula
a := math.Sin(deltaLat/2)*math.Sin(deltaLat/2) +
math.Cos(lat1Rad)*math.Cos(lat2Rad)*
math.Sin(deltaLon/2)*math.Sin(deltaLon/2)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return earthRadiusKm * c
}
// CalculateDistance calculates distance between person and location
func CalculateDistance(person PersonLocation, location Location) DistanceResult {
distance := Haversine(person.Latitude, person.Longitude, location.Latitude, location.Longitude)
return DistanceResult{
Person: person.Name + " " + person.Surname,
Location: location.Name,
DistanceKm: distance,
}
}
// FindClosestLocation finds the closest location to a person
func FindClosestLocation(person PersonLocation, locations []Location) *DistanceResult {
if len(locations) == 0 {
return nil
}
var closest *DistanceResult
minDistance := math.MaxFloat64
for _, loc := range locations {
result := CalculateDistance(person, loc)
if result.DistanceKm < minDistance {
minDistance = result.DistanceKm
closest = &result
}
}
return closest
}
// NominatimResponse represents OpenStreetMap Nominatim API response
type NominatimResponse struct {
Lat string `json:"lat"`
Lon string `json:"lon"`
}
// GetPlantGeolocation fetches accurate coordinates for a city using geocoding API
func GetPlantGeolocation(ctx context.Context, cityName string) (float64, float64, error) {
url := fmt.Sprintf("https://nominatim.openstreetmap.org/search?city=%s&country=Poland&format=json&limit=1", cityName)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return 0, 0, fmt.Errorf("creating request: %w", err)
}
// OpenStreetMap requires User-Agent header
req.Header.Set("User-Agent", "PersonProcessor/1.0")
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
return 0, 0, fmt.Errorf("making request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, 0, fmt.Errorf("reading response: %w", err)
}
var results []NominatimResponse
if err := json.Unmarshal(body, &results); err != nil {
return 0, 0, fmt.Errorf("parsing JSON: %w", err)
}
if len(results) == 0 {
return 0, 0, fmt.Errorf("no results found for city: %s", cityName)
}
// Parse coordinates
var lat, lon float64
if _, err := fmt.Sscanf(results[0].Lat, "%f", &lat); err != nil {
return 0, 0, fmt.Errorf("parsing latitude: %w", err)
}
if _, err := fmt.Sscanf(results[0].Lon, "%f", &lon); err != nil {
return 0, 0, fmt.Errorf("parsing longitude: %w", err)
}
return lat, lon, nil
}
// NearestPointResult represents result of finding nearest point
type NearestPointResult struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
DistanceKm float64 `json:"distance_km"`
Index int `json:"index"`
}
// FindNearestPoint finds the nearest point from a list to a reference point
func FindNearestPoint(referenceLat, referenceLon float64, points [][]float64) *NearestPointResult {
if len(points) == 0 {
return nil
}
var nearest *NearestPointResult
minDistance := math.MaxFloat64
for i, point := range points {
if len(point) < 2 {
continue // Skip invalid points
}
lat := point[0]
lon := point[1]
distance := Haversine(referenceLat, referenceLon, lat, lon)
if distance < minDistance {
minDistance = distance
nearest = &NearestPointResult{
Latitude: lat,
Longitude: lon,
DistanceKm: distance,
Index: i,
}
}
}
return nearest
}

View File

@@ -0,0 +1,8 @@
package domain
import "context"
// LocationRepository defines the interface for loading locations
type LocationRepository interface {
LoadLocations(ctx context.Context, apiKey string) ([]Location, error)
}

11
internal/domain/person.go Normal file
View File

@@ -0,0 +1,11 @@
package domain
// Person represents a person from the list
type Person struct {
Name string `json:"name"`
Surname string `json:"surname"`
Gender string `json:"gender"`
Born int `json:"born"`
City string `json:"city"`
Tags []string `json:"tags"`
}

View File

@@ -0,0 +1,13 @@
package domain
// PersonReport represents a complete report for one person
type PersonReport struct {
Name string `json:"name"`
Surname string `json:"surname"`
NearestPlant string `json:"nearest_plant"`
PlantCode string `json:"plant_code"`
DistanceKm float64 `json:"distance_km"`
AccessLevel int `json:"access_level"`
PrimaryLatitude float64 `json:"primary_latitude"`
PrimaryLongitude float64 `json:"primary_longitude"`
}

32
internal/domain/report.go Normal file
View File

@@ -0,0 +1,32 @@
package domain
import "sort"
// PersonWithDistance represents a person with their distance to a location
type PersonWithDistance struct {
Name string `json:"name"`
Surname string `json:"surname"`
LocationName string `json:"location_name"`
DistanceKm float64 `json:"distance_km"`
AccessLevel int `json:"access_level,omitempty"`
}
// LocationReport represents a report for a single location
type LocationReport struct {
LocationName string `json:"location_name"`
Persons []PersonWithDistance `json:"persons"`
}
// SortPersonsByDistance sorts persons by distance (ascending)
func (r *LocationReport) SortPersonsByDistance() {
sort.Slice(r.Persons, func(i, j int) bool {
return r.Persons[i].DistanceKm < r.Persons[j].DistanceKm
})
}
// PersonData holds all gathered data for a person
type PersonData struct {
Person Person
Locations []PersonLocation // Multiple possible locations
AccessLevel int
}

View File

@@ -0,0 +1,8 @@
package domain
import "context"
// PersonRepository defines the interface for loading persons
type PersonRepository interface {
LoadPersons(ctx context.Context, filePath string) ([]Person, error)
}

132
internal/domain/tools.go Normal file
View File

@@ -0,0 +1,132 @@
package domain
// GetToolDefinitions returns tool definitions for function calling
func GetToolDefinitions() []Tool {
return []Tool{
{
Type: "function",
Function: FunctionDef{
Name: "get_location",
Description: "Gets the current location information for a person. Returns latitude and longitude coordinates.",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"type": "string",
"description": "The first name of the person",
},
"surname": map[string]interface{}{
"type": "string",
"description": "The surname/last name of the person",
},
},
"required": []string{"name", "surname"},
},
},
},
{
Type: "function",
Function: FunctionDef{
Name: "get_access_level",
Description: "Gets the access level information for a person. Requires the person's birth year (only the year as integer, not full date). Returns access level data.",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"type": "string",
"description": "The first name of the person",
},
"surname": map[string]interface{}{
"type": "string",
"description": "The surname/last name of the person",
},
"birth_year": map[string]interface{}{
"type": "integer",
"description": "The birth year of the person (only the year as integer, e.g., 1987, not full date)",
},
},
"required": []string{"name", "surname", "birth_year"},
},
},
},
{
Type: "function",
Function: FunctionDef{
Name: "get_power_plant_location",
Description: "Gets the geographical coordinates (latitude and longitude) of a power plant by its city name.",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"city_name": map[string]interface{}{
"type": "string",
"description": "The name of the city where the power plant is located",
},
},
"required": []string{"city_name"},
},
},
},
{
Type: "function",
Function: FunctionDef{
Name: "calculate_distance",
Description: "Calculates the distance in kilometers between two geographical points using the Haversine formula.",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"lat1": map[string]interface{}{
"type": "number",
"description": "Latitude of the first point",
},
"lon1": map[string]interface{}{
"type": "number",
"description": "Longitude of the first point",
},
"lat2": map[string]interface{}{
"type": "number",
"description": "Latitude of the second point",
},
"lon2": map[string]interface{}{
"type": "number",
"description": "Longitude of the second point",
},
},
"required": []string{"lat1", "lon1", "lat2", "lon2"},
},
},
},
{
Type: "function",
Function: FunctionDef{
Name: "find_nearest_point",
Description: "Finds the nearest point from a list of points to a reference point (e.g., power plant location). Returns the nearest point with its distance.",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"reference_lat": map[string]interface{}{
"type": "number",
"description": "Latitude of the reference point (e.g., power plant)",
},
"reference_lon": map[string]interface{}{
"type": "number",
"description": "Longitude of the reference point (e.g., power plant)",
},
"points": map[string]interface{}{
"type": "array",
"description": "Array of points to check, each point is [latitude, longitude]",
"items": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"type": "number",
},
"minItems": 2,
"maxItems": 2,
},
},
},
"required": []string{"reference_lat", "reference_lon", "points"},
},
},
},
}
}

View File

@@ -0,0 +1,90 @@
package api
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/paramah/ai_devs4/s01e02/internal/domain"
)
// Client implements domain.APIClient
type Client struct {
locationsURL string
accessLevelURL string
httpClient *http.Client
}
// NewClient creates a new API client
func NewClient(locationsURL, accessLevelURL string) *Client {
return &Client{
locationsURL: locationsURL,
accessLevelURL: accessLevelURL,
httpClient: &http.Client{},
}
}
// GetLocation sends a location request and returns the raw JSON response
func (c *Client) GetLocation(ctx context.Context, req domain.LocationRequest) ([]byte, error) {
jsonData, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshaling request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.locationsURL, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
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 resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
}
return body, nil
}
// GetAccessLevel sends an access level request and returns the raw JSON response
func (c *Client) GetAccessLevel(ctx context.Context, req domain.AccessLevelRequest) ([]byte, error) {
jsonData, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshaling request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.accessLevelURL, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
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 resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
}
return body, nil
}

View File

@@ -0,0 +1,59 @@
package json
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/paramah/ai_devs4/s01e02/internal/domain"
)
// LocationRepository implements domain.LocationRepository
type LocationRepository struct {
baseURL string
client *http.Client
}
// NewLocationRepository creates a new location repository
func NewLocationRepository(baseURL string) *LocationRepository {
return &LocationRepository{
baseURL: baseURL,
client: &http.Client{},
}
}
// LoadLocations loads locations from the API
func (r *LocationRepository) LoadLocations(ctx context.Context, apiKey string) ([]domain.Location, error) {
url := fmt.Sprintf("%s/data/%s/findhim_locations.json", r.baseURL, apiKey)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
resp, err := r.client.Do(req)
if err != nil {
return nil, fmt.Errorf("fetching locations: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response: %w", err)
}
var plantsData domain.PowerPlantsData
if err := json.Unmarshal(body, &plantsData); err != nil {
return nil, fmt.Errorf("parsing JSON: %w", err)
}
locations := plantsData.ToLocations()
return locations, nil
}

View File

@@ -0,0 +1,33 @@
package json
import (
"context"
"encoding/json"
"fmt"
"os"
"github.com/paramah/ai_devs4/s01e02/internal/domain"
)
// Repository implements domain.PersonRepository
type Repository struct{}
// NewRepository creates a new JSON repository
func NewRepository() *Repository {
return &Repository{}
}
// LoadPersons loads persons from a JSON file
func (r *Repository) LoadPersons(ctx context.Context, filePath string) ([]domain.Person, error) {
data, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("reading file: %w", err)
}
var persons []domain.Person
if err := json.Unmarshal(data, &persons); err != nil {
return nil, fmt.Errorf("parsing JSON: %w", err)
}
return persons, nil
}

View File

@@ -0,0 +1,123 @@
package llm
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/paramah/ai_devs4/s01e02/internal/domain"
)
// LMStudioProvider implements domain.LLMProvider for local LM Studio
type LMStudioProvider struct {
baseURL string
model string
client *http.Client
}
// NewLMStudioProvider creates a new LM Studio provider
func NewLMStudioProvider(baseURL, model string) *LMStudioProvider {
return &LMStudioProvider{
baseURL: baseURL,
model: model,
client: &http.Client{},
}
}
type lmStudioRequest struct {
Model string `json:"model"`
Messages []domain.LLMMessage `json:"messages"`
Tools []domain.Tool `json:"tools,omitempty"`
ToolChoice interface{} `json:"tool_choice,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
}
type lmStudioResponse struct {
Choices []struct {
Message domain.LLMMessage `json:"message"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
Error json.RawMessage `json:"error,omitempty"`
}
// Chat sends a chat request with function calling support
func (p *LMStudioProvider) Chat(ctx context.Context, request domain.LLMRequest) (*domain.LLMResponse, error) {
reqBody := lmStudioRequest{
Model: p.model,
Messages: request.Messages,
Tools: request.Tools,
Temperature: request.Temperature,
}
if request.ToolChoice != "" {
if request.ToolChoice == "auto" {
reqBody.ToolChoice = "auto"
} else {
reqBody.ToolChoice = map[string]interface{}{
"type": "function",
"function": map[string]string{
"name": request.ToolChoice,
},
}
}
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("marshaling request: %w", err)
}
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 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))
}
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. Response body: %s", string(body))
}
return &domain.LLMResponse{
Message: apiResp.Choices[0].Message,
FinishReason: apiResp.Choices[0].FinishReason,
}, nil
}

View File

@@ -0,0 +1,113 @@
package llm
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/paramah/ai_devs4/s01e02/internal/domain"
)
// OpenRouterProvider implements domain.LLMProvider for OpenRouter API
type OpenRouterProvider struct {
apiKey string
model string
baseURL string
client *http.Client
}
// NewOpenRouterProvider creates a new OpenRouter provider
func NewOpenRouterProvider(apiKey, model string) *OpenRouterProvider {
return &OpenRouterProvider{
apiKey: apiKey,
model: model,
baseURL: "https://openrouter.ai/api/v1/chat/completions",
client: &http.Client{},
}
}
type openRouterRequest struct {
Model string `json:"model"`
Messages []domain.LLMMessage `json:"messages"`
Tools []domain.Tool `json:"tools,omitempty"`
ToolChoice interface{} `json:"tool_choice,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
}
type openRouterResponse struct {
Choices []struct {
Message domain.LLMMessage `json:"message"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
Error *struct {
Message string `json:"message"`
} `json:"error,omitempty"`
}
// Chat sends a chat request with function calling support
func (p *OpenRouterProvider) Chat(ctx context.Context, request domain.LLMRequest) (*domain.LLMResponse, error) {
reqBody := openRouterRequest{
Model: p.model,
Messages: request.Messages,
Tools: request.Tools,
Temperature: request.Temperature,
}
if request.ToolChoice != "" {
if request.ToolChoice == "auto" {
reqBody.ToolChoice = "auto"
} else {
reqBody.ToolChoice = map[string]interface{}{
"type": "function",
"function": map[string]string{
"name": request.ToolChoice,
},
}
}
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("marshaling request: %w", err)
}
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)
}
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")
}
return &domain.LLMResponse{
Message: apiResp.Choices[0].Message,
FinishReason: apiResp.Choices[0].FinishReason,
}, nil
}

View File

@@ -0,0 +1,239 @@
package usecase
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"github.com/paramah/ai_devs4/s01e02/internal/domain"
)
// AgentProcessorUseCase handles the processing of persons using LLM agent
type AgentProcessorUseCase struct {
personRepo domain.PersonRepository
locationRepo domain.LocationRepository
apiClient domain.APIClient
llmProvider domain.LLMProvider
apiKey string
outputDir string
}
// NewAgentProcessorUseCase creates a new use case instance
func NewAgentProcessorUseCase(
personRepo domain.PersonRepository,
locationRepo domain.LocationRepository,
apiClient domain.APIClient,
llmProvider domain.LLMProvider,
apiKey string,
outputDir string,
) *AgentProcessorUseCase {
return &AgentProcessorUseCase{
personRepo: personRepo,
locationRepo: locationRepo,
apiClient: apiClient,
llmProvider: llmProvider,
apiKey: apiKey,
outputDir: outputDir,
}
}
// Execute processes all persons using LLM agent
func (uc *AgentProcessorUseCase) Execute(ctx context.Context, inputFile string) error {
// Load persons from file
log.Printf("Loading persons from: %s", inputFile)
persons, err := uc.personRepo.LoadPersons(ctx, inputFile)
if err != nil {
return fmt.Errorf("loading persons: %w", err)
}
log.Printf("Loaded %d persons", len(persons))
// Load power plant locations
log.Printf("Loading power plant locations...")
locations, err := uc.locationRepo.LoadLocations(ctx, uc.apiKey)
if err != nil {
return fmt.Errorf("loading locations: %w", err)
}
log.Printf("Loaded %d power plant locations", len(locations))
// Process each person with agent
for i, person := range persons {
log.Printf("\n[%d/%d] Processing: %s %s", i+1, len(persons), person.Name, person.Surname)
if err := uc.processPerson(ctx, person, locations); err != nil {
log.Printf("Error processing %s %s: %v", person.Name, person.Surname, err)
continue
}
}
log.Printf("\nProcessing completed!")
return nil
}
// processPerson uses LLM agent to gather data for a person
func (uc *AgentProcessorUseCase) processPerson(ctx context.Context, person domain.Person, powerPlants []domain.Location) error {
tools := domain.GetToolDefinitions()
// Initial system message
systemPrompt := fmt.Sprintf(`You are an agent that gathers information about people.
For the person %s %s (born: %d), you need to:
1. Call get_location to get their current location coordinates
2. Call get_access_level to get their access level (remember: birth_year parameter must be only the year as integer, e.g., %d)
After gathering the data, respond with "DONE".`, person.Name, person.Surname, person.Born, person.Born)
messages := []domain.LLMMessage{
{
Role: "system",
Content: systemPrompt,
},
{
Role: "user",
Content: fmt.Sprintf("Please gather information for %s %s.", person.Name, person.Surname),
},
}
maxIterations := 10
var personLocation *domain.PersonLocation
for iteration := 0; iteration < maxIterations; iteration++ {
log.Printf(" [Iteration %d] Calling LLM...", iteration+1)
resp, err := uc.llmProvider.Chat(ctx, domain.LLMRequest{
Messages: messages,
Tools: tools,
ToolChoice: "auto",
Temperature: 0.0,
})
if err != nil {
return fmt.Errorf("LLM chat error: %w", err)
}
messages = append(messages, resp.Message)
// Check if LLM wants to call functions
if len(resp.Message.ToolCalls) > 0 {
log.Printf(" → LLM requested %d tool call(s)", len(resp.Message.ToolCalls))
for _, toolCall := range resp.Message.ToolCalls {
result, loc, err := uc.executeToolCall(ctx, person, toolCall)
if err != nil {
return fmt.Errorf("executing tool call: %w", err)
}
// Store person location if we got it from get_location
if loc != nil {
personLocation = loc
}
messages = append(messages, domain.LLMMessage{
Role: "tool",
Content: result,
ToolCallID: toolCall.ID,
})
}
} else if resp.FinishReason == "stop" {
log.Printf(" ✓ Agent completed gathering data")
break
}
}
// Calculate distances if we have location
if personLocation != nil && len(powerPlants) > 0 {
log.Printf(" → Calculating distances to power plants...")
closest := domain.FindClosestLocation(*personLocation, powerPlants)
if closest != nil {
log.Printf(" ✓ Closest power plant: %s (%.2f km)", closest.Location, closest.DistanceKm)
// Save distance result
distanceFile := filepath.Join(uc.outputDir, "distances", fmt.Sprintf("%s_%s.json", person.Name, person.Surname))
distanceData, _ := json.MarshalIndent(closest, "", " ")
os.WriteFile(distanceFile, distanceData, 0644)
}
}
return nil
}
// executeToolCall executes a tool call from the LLM
func (uc *AgentProcessorUseCase) executeToolCall(ctx context.Context, person domain.Person, toolCall domain.ToolCall) (string, *domain.PersonLocation, error) {
log.Printf(" → Executing: %s", toolCall.Function.Name)
var args map[string]interface{}
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
return "", nil, fmt.Errorf("parsing arguments: %w", err)
}
switch toolCall.Function.Name {
case "get_location":
name, _ := args["name"].(string)
surname, _ := args["surname"].(string)
req := domain.LocationRequest{
APIKey: uc.apiKey,
Name: name,
Surname: surname,
}
response, err := uc.apiClient.GetLocation(ctx, req)
if err != nil {
return fmt.Sprintf("Error: %v", err), nil, nil
}
// Save response
fileName := fmt.Sprintf("%s_%s.json", name, surname)
filePath := filepath.Join(uc.outputDir, "locations", fileName)
os.WriteFile(filePath, response, 0644)
log.Printf(" ✓ Saved to: %s", filePath)
// Parse location to get coordinates (API returns array of locations)
var locationData []map[string]interface{}
if err := json.Unmarshal(response, &locationData); err == nil && len(locationData) > 0 {
// Take first location from the array
firstLoc := locationData[0]
if lat, ok := firstLoc["latitude"].(float64); ok {
if lon, ok := firstLoc["longitude"].(float64); ok {
personLoc := &domain.PersonLocation{
Name: name,
Surname: surname,
Latitude: lat,
Longitude: lon,
}
return string(response), personLoc, nil
}
}
}
return string(response), nil, nil
case "get_access_level":
name, _ := args["name"].(string)
surname, _ := args["surname"].(string)
birthYear, _ := args["birth_year"].(float64)
req := domain.AccessLevelRequest{
APIKey: uc.apiKey,
Name: name,
Surname: surname,
BirthYear: int(birthYear),
}
response, err := uc.apiClient.GetAccessLevel(ctx, req)
if err != nil {
return fmt.Sprintf("Error: %v", err), nil, nil
}
// Save response
fileName := fmt.Sprintf("%s_%s.json", name, surname)
filePath := filepath.Join(uc.outputDir, "accesslevel", fileName)
os.WriteFile(filePath, response, 0644)
log.Printf(" ✓ Saved to: %s", filePath)
return string(response), nil, nil
default:
return fmt.Sprintf("Unknown function: %s", toolCall.Function.Name), nil, nil
}
}

View File

@@ -0,0 +1,185 @@
package usecase
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/paramah/ai_devs4/s01e02/internal/domain"
)
// FindClosestPowerPlantUseCase uses LLM agent to find the closest power plant
type FindClosestPowerPlantUseCase struct {
llmProvider domain.LLMProvider
}
// NewFindClosestPowerPlantUseCase creates a new use case
func NewFindClosestPowerPlantUseCase(llmProvider domain.LLMProvider) *FindClosestPowerPlantUseCase {
return &FindClosestPowerPlantUseCase{
llmProvider: llmProvider,
}
}
// Execute uses agent to find the closest power plant to any person
func (uc *FindClosestPowerPlantUseCase) Execute(
ctx context.Context,
personDataMap map[string]*domain.PersonData,
plantsData *domain.PowerPlantsData,
) (*domain.PersonData, string, float64, string, error) {
// Prepare data summary for agent
personsInfo := ""
for _, pd := range personDataMap {
if len(pd.Locations) > 0 {
loc := pd.Locations[0]
personsInfo += fmt.Sprintf("- %s %s: lat=%.4f, lon=%.4f, access_level=%d\n",
pd.Person.Name, pd.Person.Surname, loc.Latitude, loc.Longitude, pd.AccessLevel)
}
}
plantsInfo := ""
for cityName := range plantsData.PowerPlants {
plantsInfo += fmt.Sprintf("- %s\n", cityName)
}
systemPrompt := fmt.Sprintf(`You are an agent that finds the closest power plant to any person.
Available persons and their locations:
%s
Available power plants (cities):
%s
Your task:
1. For each power plant, call get_power_plant_location to get its coordinates
2. For each person-plant pair, call calculate_distance to find the distance
3. Track the minimum distance and corresponding person/plant
4. After checking all combinations, respond with JSON containing the result:
{
"person_name": "Name",
"person_surname": "Surname",
"plant_city": "City",
"min_distance": 123.45
}`, personsInfo, plantsInfo)
tools := domain.GetToolDefinitions()
messages := []domain.LLMMessage{
{
Role: "system",
Content: systemPrompt,
},
{
Role: "user",
Content: "Find the power plant that is closest to any person. Check all combinations.",
},
}
maxIterations := 100 // Need many iterations for all combinations
var resultJSON string
for iteration := 0; iteration < maxIterations; iteration++ {
resp, err := uc.llmProvider.Chat(ctx, domain.LLMRequest{
Messages: messages,
Tools: tools,
ToolChoice: "auto",
Temperature: 0.0,
})
if err != nil {
return nil, "", 0, "", fmt.Errorf("LLM chat error: %w", err)
}
messages = append(messages, resp.Message)
if len(resp.Message.ToolCalls) > 0 {
log.Printf(" [Iteration %d] Agent requested %d tool call(s)", iteration+1, len(resp.Message.ToolCalls))
for _, toolCall := range resp.Message.ToolCalls {
result := uc.executeToolCall(toolCall, plantsData)
messages = append(messages, domain.LLMMessage{
Role: "tool",
Content: result,
ToolCallID: toolCall.ID,
})
}
} else if resp.FinishReason == "stop" {
resultJSON = resp.Message.Content
log.Printf(" ✓ Agent completed analysis")
log.Printf(" → Raw response: %s", resultJSON)
break
}
}
// Extract JSON from response (agent might wrap it in text)
start := -1
end := -1
for i, ch := range resultJSON {
if ch == '{' && start == -1 {
start = i
}
if ch == '}' {
end = i + 1
}
}
if start == -1 || end == -1 {
return nil, "", 0, "", fmt.Errorf("no JSON found in response: %s", resultJSON)
}
jsonStr := resultJSON[start:end]
// Parse result
var result struct {
PersonName string `json:"person_name"`
PersonSurname string `json:"person_surname"`
PlantCity string `json:"plant_city"`
MinDistance float64 `json:"min_distance"`
}
if err := json.Unmarshal([]byte(jsonStr), &result); err != nil {
return nil, "", 0, "", fmt.Errorf("parsing result: %w (json: %s)", err, jsonStr)
}
// Find the person data
key := fmt.Sprintf("%s_%s", result.PersonName, result.PersonSurname)
personData, ok := personDataMap[key]
if !ok {
return nil, "", 0, "", fmt.Errorf("person not found: %s", key)
}
// Get plant code
plantInfo, ok := plantsData.PowerPlants[result.PlantCity]
if !ok {
return nil, "", 0, "", fmt.Errorf("plant not found: %s", result.PlantCity)
}
return personData, result.PlantCity, result.MinDistance, plantInfo.Code, nil
}
// executeToolCall handles tool execution
func (uc *FindClosestPowerPlantUseCase) executeToolCall(toolCall domain.ToolCall, plantsData *domain.PowerPlantsData) string {
var args map[string]interface{}
json.Unmarshal([]byte(toolCall.Function.Arguments), &args)
switch toolCall.Function.Name {
case "get_power_plant_location":
cityName, _ := args["city_name"].(string)
coords, ok := domain.CityCoordinates[cityName]
if !ok {
return fmt.Sprintf(`{"error": "City not found: %s"}`, cityName)
}
return fmt.Sprintf(`{"city": "%s", "lat": %.6f, "lon": %.6f}`, cityName, coords.Lat, coords.Lon)
case "calculate_distance":
lat1, _ := args["lat1"].(float64)
lon1, _ := args["lon1"].(float64)
lat2, _ := args["lat2"].(float64)
lon2, _ := args["lon2"].(float64)
distance := domain.Haversine(lat1, lon1, lat2, lon2)
return fmt.Sprintf(`{"distance_km": %.2f}`, distance)
default:
return fmt.Sprintf(`{"error": "Unknown function: %s"}`, toolCall.Function.Name)
}
}

View File

@@ -0,0 +1,461 @@
package usecase
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"github.com/paramah/ai_devs4/s01e02/internal/domain"
)
// OptimizedAgentProcessorUseCase handles optimized processing with location reports
type OptimizedAgentProcessorUseCase struct {
personRepo domain.PersonRepository
locationRepo domain.LocationRepository
apiClient domain.APIClient
llmProvider domain.LLMProvider
apiKey string
outputDir string
}
// NewOptimizedAgentProcessorUseCase creates a new optimized use case instance
func NewOptimizedAgentProcessorUseCase(
personRepo domain.PersonRepository,
locationRepo domain.LocationRepository,
apiClient domain.APIClient,
llmProvider domain.LLMProvider,
apiKey string,
outputDir string,
) *OptimizedAgentProcessorUseCase {
return &OptimizedAgentProcessorUseCase{
personRepo: personRepo,
locationRepo: locationRepo,
apiClient: apiClient,
llmProvider: llmProvider,
apiKey: apiKey,
outputDir: outputDir,
}
}
// Execute processes all persons and generates location reports
func (uc *OptimizedAgentProcessorUseCase) Execute(ctx context.Context, inputFile string) error {
// Load persons from file
log.Printf("Loading persons from: %s", inputFile)
persons, err := uc.personRepo.LoadPersons(ctx, inputFile)
if err != nil {
return fmt.Errorf("loading persons: %w", err)
}
log.Printf("Loaded %d persons", len(persons))
// Load power plant locations
log.Printf("Loading power plant locations...")
powerPlants, err := uc.locationRepo.LoadLocations(ctx, uc.apiKey)
if err != nil {
return fmt.Errorf("loading locations: %w", err)
}
log.Printf("Loaded %d power plant locations", len(powerPlants))
// Also load raw power plants data for codes
plantsData, err := uc.loadPowerPlantsData(ctx)
if err != nil {
return fmt.Errorf("loading power plants data: %w", err)
}
// Save power plants data to output
plantsDataJSON, err := json.MarshalIndent(plantsData, "", " ")
if err != nil {
log.Printf("Warning: Failed to marshal power plants data: %v", err)
} else {
plantsFilePath := filepath.Join(uc.outputDir, "findhim_locations.json")
if err := os.WriteFile(plantsFilePath, plantsDataJSON, 0644); err != nil {
log.Printf("Warning: Failed to save power plants data: %v", err)
} else {
log.Printf("✓ Power plants data saved to: %s", plantsFilePath)
}
}
// Phase 1: Gather all data for all persons
log.Printf("\n========== Phase 1: Gathering person data ==========")
personDataMap := make(map[string]*domain.PersonData)
for i, person := range persons {
log.Printf("[%d/%d] Gathering data for: %s %s", i+1, len(persons), person.Name, person.Surname)
personData, err := uc.gatherPersonData(ctx, person)
if err != nil {
log.Printf("Error gathering data for %s %s: %v", person.Name, person.Surname, err)
continue
}
key := fmt.Sprintf("%s_%s", person.Name, person.Surname)
personDataMap[key] = personData
}
// Phase 2: Calculate all distances and generate location reports
log.Printf("\n========== Phase 2: Generating location reports ==========")
locationReports := uc.generateLocationReports(personDataMap, powerPlants)
// Phase 3: Save reports
log.Printf("\n========== Phase 3: Saving reports ==========")
for _, report := range locationReports {
uc.saveLocationReport(report)
}
// Phase 4: Find closest power plant (locally, no LLM needed)
log.Printf("\n========== Phase 4: Finding closest power plant ==========")
closestPerson, closestPlantName, distance, plantCode := uc.findClosestPowerPlantLocally(personDataMap, powerPlants, plantsData)
if closestPerson != nil {
log.Printf(" ✓ Closest power plant: %s (%s)", closestPlantName, plantCode)
log.Printf(" ✓ Closest person: %s %s", closestPerson.Person.Name, closestPerson.Person.Surname)
log.Printf(" ✓ Distance: %.2f km", distance)
log.Printf(" ✓ Access level: %d", closestPerson.AccessLevel)
// Save final answer
finalAnswer := domain.FinalAnswer{
APIKey: uc.apiKey,
Task: "findhim",
Answer: domain.AnswerDetail{
Name: closestPerson.Person.Name,
Surname: closestPerson.Person.Surname,
AccessLevel: closestPerson.AccessLevel,
PowerPlant: plantCode,
},
}
answerJSON, err := json.MarshalIndent(finalAnswer, "", " ")
if err != nil {
return fmt.Errorf("marshaling final answer: %w", err)
}
answerPath := filepath.Join(uc.outputDir, "final_answer.json")
if err := os.WriteFile(answerPath, answerJSON, 0644); err != nil {
return fmt.Errorf("saving final answer: %w", err)
}
log.Printf(" ✓ Final answer saved to: %s", answerPath)
}
log.Printf("\nProcessing completed!")
return nil
}
// gatherPersonData uses LLM agent to gather all data for a person (optimized)
func (uc *OptimizedAgentProcessorUseCase) gatherPersonData(ctx context.Context, person domain.Person) (*domain.PersonData, error) {
tools := domain.GetToolDefinitions()
// Minimal, optimized system prompt
systemPrompt := fmt.Sprintf(`Gather data for person: %s %s (born: %d).
Tasks:
1. Call get_location
2. Call get_access_level (use birth_year: %d)
Then respond "DONE".`, person.Name, person.Surname, person.Born, person.Born)
messages := []domain.LLMMessage{
{
Role: "system",
Content: systemPrompt,
},
{
Role: "user",
Content: "Start gathering data.",
},
}
maxIterations := 5
var personLocations []domain.PersonLocation
var accessLevel int
for iteration := 0; iteration < maxIterations; iteration++ {
resp, err := uc.llmProvider.Chat(ctx, domain.LLMRequest{
Messages: messages,
Tools: tools,
ToolChoice: "auto",
Temperature: 0.0,
})
if err != nil {
return nil, fmt.Errorf("LLM chat error: %w", err)
}
messages = append(messages, resp.Message)
if len(resp.Message.ToolCalls) > 0 {
for _, toolCall := range resp.Message.ToolCalls {
result, locations, level, err := uc.executeToolCall(ctx, person, toolCall)
if err != nil {
return nil, fmt.Errorf("executing tool call: %w", err)
}
personLocations = append(personLocations, locations...)
if level > 0 {
accessLevel = level
}
messages = append(messages, domain.LLMMessage{
Role: "tool",
Content: result,
ToolCallID: toolCall.ID,
})
}
} else if resp.FinishReason == "stop" {
log.Printf(" ✓ Data gathered (locations: %d, access level: %d)", len(personLocations), accessLevel)
break
}
}
return &domain.PersonData{
Person: person,
Locations: personLocations,
AccessLevel: accessLevel,
}, nil
}
// executeToolCall executes a tool call and returns locations and access level
func (uc *OptimizedAgentProcessorUseCase) executeToolCall(
ctx context.Context,
person domain.Person,
toolCall domain.ToolCall,
) (string, []domain.PersonLocation, int, error) {
var args map[string]interface{}
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
return "", nil, 0, fmt.Errorf("parsing arguments: %w", err)
}
switch toolCall.Function.Name {
case "get_location":
name, _ := args["name"].(string)
surname, _ := args["surname"].(string)
req := domain.LocationRequest{
APIKey: uc.apiKey,
Name: name,
Surname: surname,
}
response, err := uc.apiClient.GetLocation(ctx, req)
if err != nil {
return fmt.Sprintf("Error: %v", err), nil, 0, nil
}
// Save response
fileName := fmt.Sprintf("%s_%s.json", name, surname)
filePath := filepath.Join(uc.outputDir, "locations", fileName)
os.WriteFile(filePath, response, 0644)
// Parse all locations from array
var locationData []map[string]interface{}
var locations []domain.PersonLocation
if err := json.Unmarshal(response, &locationData); err == nil {
for _, loc := range locationData {
if lat, ok := loc["latitude"].(float64); ok {
if lon, ok := loc["longitude"].(float64); ok {
locations = append(locations, domain.PersonLocation{
Name: name,
Surname: surname,
Latitude: lat,
Longitude: lon,
})
}
}
}
}
log.Printf(" → get_location: %d locations found", len(locations))
return string(response), locations, 0, nil
case "get_access_level":
name, _ := args["name"].(string)
surname, _ := args["surname"].(string)
birthYear, _ := args["birth_year"].(float64)
req := domain.AccessLevelRequest{
APIKey: uc.apiKey,
Name: name,
Surname: surname,
BirthYear: int(birthYear),
}
response, err := uc.apiClient.GetAccessLevel(ctx, req)
if err != nil {
return fmt.Sprintf("Error: %v", err), nil, 0, nil
}
// Save response
fileName := fmt.Sprintf("%s_%s.json", name, surname)
filePath := filepath.Join(uc.outputDir, "accesslevel", fileName)
os.WriteFile(filePath, response, 0644)
// Parse access level
var accessData struct {
AccessLevel int `json:"accessLevel"`
}
var level int
if err := json.Unmarshal(response, &accessData); err == nil {
level = accessData.AccessLevel
}
log.Printf(" → get_access_level: level %d", level)
return string(response), nil, level, nil
default:
return fmt.Sprintf("Unknown function: %s", toolCall.Function.Name), nil, 0, nil
}
}
// generateLocationReports creates reports for each power plant location
func (uc *OptimizedAgentProcessorUseCase) generateLocationReports(
personDataMap map[string]*domain.PersonData,
powerPlants []domain.Location,
) []domain.LocationReport {
var reports []domain.LocationReport
for _, powerPlant := range powerPlants {
report := domain.LocationReport{
LocationName: powerPlant.Name,
Persons: []domain.PersonWithDistance{},
}
// Calculate distances for all persons to this power plant
for _, personData := range personDataMap {
if len(personData.Locations) == 0 {
continue
}
// Use first location (primary location)
personLoc := personData.Locations[0]
distance := domain.Haversine(
personLoc.Latitude,
personLoc.Longitude,
powerPlant.Latitude,
powerPlant.Longitude,
)
report.Persons = append(report.Persons, domain.PersonWithDistance{
Name: personData.Person.Name,
Surname: personData.Person.Surname,
LocationName: powerPlant.Name,
DistanceKm: distance,
AccessLevel: personData.AccessLevel,
})
}
// Sort by distance
report.SortPersonsByDistance()
reports = append(reports, report)
log.Printf(" ✓ %s: %d persons", powerPlant.Name, len(report.Persons))
}
return reports
}
// saveLocationReport saves a location report to file
func (uc *OptimizedAgentProcessorUseCase) saveLocationReport(report domain.LocationReport) {
fileName := fmt.Sprintf("%s_report.json", report.LocationName)
filePath := filepath.Join(uc.outputDir, "reports", fileName)
data, err := json.MarshalIndent(report, "", " ")
if err != nil {
log.Printf("Error marshaling report for %s: %v", report.LocationName, err)
return
}
if err := os.WriteFile(filePath, data, 0644); err != nil {
log.Printf("Error saving report for %s: %v", report.LocationName, err)
return
}
log.Printf(" ✓ Saved: %s", filePath)
}
// loadPowerPlantsData loads raw power plants data with codes
func (uc *OptimizedAgentProcessorUseCase) loadPowerPlantsData(ctx context.Context) (*domain.PowerPlantsData, error) {
// Use the location repository to get the raw data
// We need to fetch from the API directly
url := fmt.Sprintf("https://hub.ag3nts.org/data/%s/findhim_locations.json", uc.apiKey)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("fetching data: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response: %w", err)
}
var plantsData domain.PowerPlantsData
if err := json.Unmarshal(body, &plantsData); err != nil {
return nil, fmt.Errorf("parsing JSON: %w", err)
}
return &plantsData, nil
}
// findClosestPowerPlantLocally finds the closest power plant without using LLM
func (uc *OptimizedAgentProcessorUseCase) findClosestPowerPlantLocally(
personDataMap map[string]*domain.PersonData,
powerPlants []domain.Location,
plantsData *domain.PowerPlantsData,
) (*domain.PersonData, string, float64, string) {
var closestPerson *domain.PersonData
var closestPlantName string
var plantCode string
minDistance := 1e10
// Check all person-plant combinations
for _, personData := range personDataMap {
if len(personData.Locations) == 0 {
continue
}
// Use first location (primary)
personLoc := personData.Locations[0]
for _, plant := range powerPlants {
distance := domain.Haversine(
personLoc.Latitude,
personLoc.Longitude,
plant.Latitude,
plant.Longitude,
)
// If distance is smaller, or same distance but higher access level
if distance < minDistance {
minDistance = distance
closestPerson = personData
closestPlantName = plant.Name
// Get power plant code
if info, ok := plantsData.PowerPlants[plant.Name]; ok {
plantCode = info.Code
}
} else if distance == minDistance && closestPerson != nil && personData.AccessLevel > closestPerson.AccessLevel {
// Same distance, but higher access level
closestPerson = personData
closestPlantName = plant.Name
// Get power plant code
if info, ok := plantsData.PowerPlants[plant.Name]; ok {
plantCode = info.Code
}
}
}
}
return closestPerson, closestPlantName, minDistance, plantCode
}

View File

@@ -0,0 +1,694 @@
package usecase
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"sync"
"github.com/paramah/ai_devs4/s01e02/internal/domain"
)
// PersonAgentProcessorUseCase processes each person with optimized LLM calls
type PersonAgentProcessorUseCase struct {
personRepo domain.PersonRepository
apiClient domain.APIClient
llmProvider domain.LLMProvider
apiKey string
outputDir string
}
// NewPersonAgentProcessorUseCase creates a new use case instance
func NewPersonAgentProcessorUseCase(
personRepo domain.PersonRepository,
apiClient domain.APIClient,
llmProvider domain.LLMProvider,
apiKey string,
outputDir string,
) *PersonAgentProcessorUseCase {
return &PersonAgentProcessorUseCase{
personRepo: personRepo,
apiClient: apiClient,
llmProvider: llmProvider,
apiKey: apiKey,
outputDir: outputDir,
}
}
// Execute processes all persons
func (uc *PersonAgentProcessorUseCase) Execute(ctx context.Context, inputFile string) error {
log.Printf("\n╔════════════════════════════════════════════════════════════════")
log.Printf("║ PHASE: INITIALIZATION")
log.Printf("╚════════════════════════════════════════════════════════════════")
// Create output directories
log.Printf("\n→ Creating output directories...")
dirs := []string{
filepath.Join(uc.outputDir, "locations"),
filepath.Join(uc.outputDir, "accesslevel"),
filepath.Join(uc.outputDir, "person_reports"),
}
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("creating directory %s: %w", dir, err)
}
log.Printf(" ✓ Created: %s", dir)
}
log.Printf("\n╔════════════════════════════════════════════════════════════════")
log.Printf("║ PHASE: LOADING DATA")
log.Printf("╚════════════════════════════════════════════════════════════════")
// Load persons
log.Printf("\n→ Loading persons from: %s", inputFile)
persons, err := uc.personRepo.LoadPersons(ctx, inputFile)
if err != nil {
return fmt.Errorf("loading persons: %w", err)
}
log.Printf("✓ Loaded %d persons", len(persons))
for i, p := range persons {
log.Printf(" %d. %s %s (born: %d, city: %s)", i+1, p.Name, p.Surname, p.Born, p.City)
}
// Load power plants data
log.Printf("\n→ Loading power plants data from API...")
plantsData, err := uc.loadPowerPlantsData(ctx)
if err != nil {
return fmt.Errorf("loading power plants: %w", err)
}
// Save power plants data
plantsJSON, _ := json.MarshalIndent(plantsData, "", " ")
plantsPath := filepath.Join(uc.outputDir, "findhim_locations.json")
os.WriteFile(plantsPath, plantsJSON, 0644)
log.Printf("✓ Loaded %d power plants", len(plantsData.PowerPlants))
// Get list of plant cities for agent
var plantCities []string
activePlants := 0
for city, info := range plantsData.PowerPlants {
plantCities = append(plantCities, city)
if info.IsActive {
activePlants++
log.Printf(" • %s (%s) - %s [ACTIVE]", city, info.Code, info.Power)
} else {
log.Printf(" • %s (%s) - %s [INACTIVE]", city, info.Code, info.Power)
}
}
log.Printf("✓ Active plants: %d/%d", activePlants, len(plantsData.PowerPlants))
log.Printf("✓ Power plants data saved to: %s", plantsPath)
// Fetch accurate geolocation for all power plants using geocoding API
log.Printf("\n→ Fetching accurate geolocations from geocoding API...")
plantCoordinates, err := uc.fetchPlantGeolocations(ctx, plantsData)
if err != nil {
return fmt.Errorf("fetching plant geolocations: %w", err)
}
log.Printf("✓ Fetched accurate coordinates for %d plants", len(plantCoordinates))
// Process each person
var personReports []domain.PersonReport
log.Printf("\n╔════════════════════════════════════════════════════════════════")
log.Printf("║ PHASE: PROCESSING PERSONS (OPTIMIZED)")
log.Printf("║ Strategy: 2 LLM calls per person + parallel distance calculation")
log.Printf("╚════════════════════════════════════════════════════════════════")
for i, person := range persons {
log.Printf("\n")
log.Printf("┌────────────────────────────────────────────────────────────────")
log.Printf("│ [%d/%d] PERSON: %s %s", i+1, len(persons), person.Name, person.Surname)
log.Printf("│ Born: %d | City: %s", person.Born, person.City)
log.Printf("└────────────────────────────────────────────────────────────────")
report, err := uc.processPerson(ctx, person, plantsData, plantCoordinates)
if err != nil {
log.Printf("\n✗✗✗ ERROR processing %s %s: %v", person.Name, person.Surname, err)
continue
}
personReports = append(personReports, *report)
// Save individual report
reportJSON, _ := json.MarshalIndent(report, "", " ")
reportPath := filepath.Join(uc.outputDir, "person_reports", fmt.Sprintf("%s_%s.json", person.Name, person.Surname))
os.WriteFile(reportPath, reportJSON, 0644)
log.Printf("\n✓ Individual report saved: %s", reportPath)
}
// Find person with minimum distance to their nearest plant
log.Printf("\n")
log.Printf("╔════════════════════════════════════════════════════════════════")
log.Printf("║ PHASE: FINDING MINIMUM DISTANCE")
log.Printf("╚════════════════════════════════════════════════════════════════")
log.Printf("\nAnalyzing all %d person reports to find minimum distance...", len(personReports))
var closestReport *domain.PersonReport
minDistance := 1e10
for i := range personReports {
log.Printf(" • %s %s → %s: %.2f km (access level: %d)",
personReports[i].Name,
personReports[i].Surname,
personReports[i].NearestPlant,
personReports[i].DistanceKm,
personReports[i].AccessLevel)
if personReports[i].DistanceKm < minDistance {
minDistance = personReports[i].DistanceKm
closestReport = &personReports[i]
}
}
if closestReport != nil {
log.Printf("\n╔════════════════════════════════════════════════════════════════")
log.Printf("║ WINNER: CLOSEST PERSON-PLANT PAIR")
log.Printf("╚════════════════════════════════════════════════════════════════")
log.Printf(" Person: %s %s", closestReport.Name, closestReport.Surname)
log.Printf(" Power Plant: %s (%s)", closestReport.NearestPlant, closestReport.PlantCode)
log.Printf(" Distance: %.2f km", closestReport.DistanceKm)
log.Printf(" Access Level: %d", closestReport.AccessLevel)
log.Printf(" Coordinates: %.4f°N, %.4f°E", closestReport.PrimaryLatitude, closestReport.PrimaryLongitude)
// Save final answer
finalAnswer := domain.FinalAnswer{
APIKey: uc.apiKey,
Task: "findhim",
Answer: domain.AnswerDetail{
Name: closestReport.Name,
Surname: closestReport.Surname,
AccessLevel: closestReport.AccessLevel,
PowerPlant: closestReport.PlantCode,
},
}
answerJSON, _ := json.MarshalIndent(finalAnswer, "", " ")
answerPath := filepath.Join(uc.outputDir, "final_answer.json")
os.WriteFile(answerPath, answerJSON, 0644)
log.Printf("\n✓ Final answer saved to: %s", answerPath)
log.Printf("\n╔════════════════════════════════════════════════════════════════")
log.Printf("║ Ready for verification!")
log.Printf("║ Run: curl -X POST https://hub.ag3nts.org/verify \\")
log.Printf("║ -H \"Content-Type: application/json\" \\")
log.Printf("║ -d @output/final_answer.json")
log.Printf("╚════════════════════════════════════════════════════════════════")
}
log.Printf("\n✓✓✓ Processing completed successfully! ✓✓✓")
return nil
}
// processPerson processes one person - OPTIMIZED: only 2 LLM calls
func (uc *PersonAgentProcessorUseCase) processPerson(
ctx context.Context,
person domain.Person,
plantsData *domain.PowerPlantsData,
plantCoordinates map[string]struct{ Lat, Lon float64 },
) (*domain.PersonReport, error) {
log.Printf(" ┌─ Starting optimized analysis for: %s %s (born: %d)", person.Name, person.Surname, person.Born)
log.Printf(" │ Strategy: LLM call 1 (locations) + parallel distance calc + LLM call 2 (access)")
// PHASE 1: Get person locations (LLM CALL 1)
log.Printf(" │")
log.Printf(" │ [LLM Call 1/2] Fetching person locations via function calling...")
personLocations, err := uc.getPersonLocations(ctx, person)
if err != nil {
return nil, fmt.Errorf("getting locations: %w", err)
}
log.Printf(" │ ✓ Found %d location(s) for %s %s", len(personLocations), person.Name, person.Surname)
if len(personLocations) == 0 {
return nil, fmt.Errorf("no locations found for person")
}
// PHASE 2: Calculate distances for ALL person locations to ALL power plants
log.Printf(" │")
log.Printf(" │ [Local Processing] Analyzing ALL %d location(s) of the person...", len(personLocations))
// Find globally nearest plant across ALL person locations
var nearestPlant string
var minDistance float64 = 1e10
var bestLocation domain.PersonLocation
for i, personLoc := range personLocations {
log.Printf(" │ ├─ Location [%d/%d]: %.4f°N, %.4f°E", i+1, len(personLocations), personLoc.Latitude, personLoc.Longitude)
log.Printf(" │ │ ┌─ Checking distances to all plants:")
plant, dist := uc.findNearestPlantParallel(personLoc, plantsData, plantCoordinates)
log.Printf(" │ │ └─ Nearest plant for this location: %s (%.2f km)", plant, dist)
if dist < minDistance {
minDistance = dist
nearestPlant = plant
bestLocation = personLoc
log.Printf(" │ │ ★ NEW MINIMUM! Updated global minimum")
}
}
log.Printf(" │ └─ ✓ Global analysis complete")
log.Printf(" │ ✓ BEST result: %s at %.2f km (from location %.4f°N, %.4f°E)", nearestPlant, minDistance, bestLocation.Latitude, bestLocation.Longitude)
// PHASE 3: Get access level (LLM CALL 2)
log.Printf(" │")
log.Printf(" │ [LLM Call 2/2] Fetching access level via function calling...")
accessLevel, err := uc.getAccessLevel(ctx, person)
if err != nil {
return nil, fmt.Errorf("getting access level: %w", err)
}
log.Printf(" │ ✓ Access level: %d", accessLevel)
// Get plant code
plantInfo, ok := plantsData.PowerPlants[nearestPlant]
if !ok {
log.Printf(" │ ✗ Plant not found in database: %s", nearestPlant)
return nil, fmt.Errorf("plant not found: %s", nearestPlant)
}
log.Printf(" │")
log.Printf(" │ ═══ FINAL RESULTS ═══")
log.Printf(" │ Nearest plant: %s (%s)", nearestPlant, plantInfo.Code)
log.Printf(" │ Distance: %.2f km", minDistance)
log.Printf(" │ Access level: %d", accessLevel)
log.Printf(" │ Best location: %.4f°N, %.4f°E", bestLocation.Latitude, bestLocation.Longitude)
log.Printf(" │ Total LLM calls: 2")
report := &domain.PersonReport{
Name: person.Name,
Surname: person.Surname,
NearestPlant: nearestPlant,
PlantCode: plantInfo.Code,
DistanceKm: minDistance,
AccessLevel: accessLevel,
PrimaryLatitude: bestLocation.Latitude,
PrimaryLongitude: bestLocation.Longitude,
}
log.Printf(" └─ ✓ Successfully processed: %s %s (2 LLM calls total)", person.Name, person.Surname)
return report, nil
}
// getPersonLocations fetches locations for a person using LLM function calling (1 call)
func (uc *PersonAgentProcessorUseCase) getPersonLocations(ctx context.Context, person domain.Person) ([]domain.PersonLocation, error) {
tools := []domain.Tool{
{
Type: "function",
Function: domain.FunctionDef{
Name: "get_location",
Description: "Gets the location information for a person by their name and surname",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"type": "string",
"description": "The first name of the person",
},
"surname": map[string]interface{}{
"type": "string",
"description": "The surname/last name of the person",
},
},
"required": []string{"name", "surname"},
},
},
},
}
systemPrompt := fmt.Sprintf("Call get_location for %s %s and return the result.", person.Name, person.Surname)
messages := []domain.LLMMessage{
{Role: "system", Content: systemPrompt},
{Role: "user", Content: "Get location now."},
}
resp, err := uc.llmProvider.Chat(ctx, domain.LLMRequest{
Messages: messages,
Tools: tools,
ToolChoice: "auto",
Temperature: 0.0,
})
if err != nil {
return nil, fmt.Errorf("LLM call failed: %w", err)
}
if len(resp.Message.ToolCalls) == 0 {
return nil, fmt.Errorf("no tool calls in response")
}
// Execute get_location
toolCall := resp.Message.ToolCalls[0]
var args map[string]interface{}
json.Unmarshal([]byte(toolCall.Function.Arguments), &args)
name, _ := args["name"].(string)
surname, _ := args["surname"].(string)
log.Printf(" │ → API call: get_location(%s, %s)", name, surname)
req := domain.LocationRequest{
APIKey: uc.apiKey,
Name: name,
Surname: surname,
}
response, err := uc.apiClient.GetLocation(ctx, req)
if err != nil {
return nil, fmt.Errorf("API call failed: %w", err)
}
// Save to file
fileName := fmt.Sprintf("%s_%s.json", name, surname)
filePath := filepath.Join(uc.outputDir, "locations", fileName)
os.WriteFile(filePath, response, 0644)
// Parse locations
var locationData []map[string]interface{}
var locations []domain.PersonLocation
if err := json.Unmarshal(response, &locationData); err != nil {
return nil, fmt.Errorf("parsing locations: %w", err)
}
for _, loc := range locationData {
if lat, ok := loc["latitude"].(float64); ok {
if lon, ok := loc["longitude"].(float64); ok {
locations = append(locations, domain.PersonLocation{
Name: name,
Surname: surname,
Latitude: lat,
Longitude: lon,
})
}
}
}
return locations, nil
}
// findNearestPlantParallel finds the nearest power plant using parallel goroutines (NO LLM)
func (uc *PersonAgentProcessorUseCase) findNearestPlantParallel(
personLoc domain.PersonLocation,
plantsData *domain.PowerPlantsData,
plantCoordinates map[string]struct{ Lat, Lon float64 },
) (string, float64) {
type result struct {
plant string
distance float64
}
results := make(chan result, len(plantsData.PowerPlants))
var wg sync.WaitGroup
// Launch goroutine for each power plant
for city, info := range plantsData.PowerPlants {
if !info.IsActive {
continue // Skip inactive plants
}
wg.Add(1)
go func(cityName string) {
defer wg.Done()
coords, ok := plantCoordinates[cityName]
if !ok {
log.Printf(" │ │ ✗ Goroutine [%s]: no coordinates found", cityName)
results <- result{cityName, 1e10}
return
}
distance := domain.Haversine(
personLoc.Latitude,
personLoc.Longitude,
coords.Lat,
coords.Lon,
)
log.Printf(" │ │ • Goroutine [%s]: %.2f km (coords: %.4f°N, %.4f°E)", cityName, distance, coords.Lat, coords.Lon)
results <- result{cityName, distance}
}(city)
}
// Close channel after all goroutines finish
go func() {
wg.Wait()
close(results)
}()
// Collect results and find minimum
minDistance := 1e10
nearestPlant := ""
for r := range results {
if r.distance < minDistance {
minDistance = r.distance
nearestPlant = r.plant
}
}
return nearestPlant, minDistance
}
// getAccessLevel fetches access level for a person using LLM function calling (1 call)
func (uc *PersonAgentProcessorUseCase) getAccessLevel(ctx context.Context, person domain.Person) (int, error) {
tools := []domain.Tool{
{
Type: "function",
Function: domain.FunctionDef{
Name: "get_access_level",
Description: "Gets the access level for a person",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"type": "string",
"description": "The first name of the person",
},
"surname": map[string]interface{}{
"type": "string",
"description": "The surname/last name of the person",
},
"birth_year": map[string]interface{}{
"type": "integer",
"description": "The birth year of the person (only year, not full date)",
},
},
"required": []string{"name", "surname", "birth_year"},
},
},
},
}
systemPrompt := fmt.Sprintf("Call get_access_level for %s %s (birth year: %d).", person.Name, person.Surname, person.Born)
messages := []domain.LLMMessage{
{Role: "system", Content: systemPrompt},
{Role: "user", Content: "Get access level now."},
}
resp, err := uc.llmProvider.Chat(ctx, domain.LLMRequest{
Messages: messages,
Tools: tools,
ToolChoice: "auto",
Temperature: 0.0,
})
if err != nil {
return 0, fmt.Errorf("LLM call failed: %w", err)
}
if len(resp.Message.ToolCalls) == 0 {
return 0, fmt.Errorf("no tool calls in response")
}
// Execute get_access_level
toolCall := resp.Message.ToolCalls[0]
var args map[string]interface{}
json.Unmarshal([]byte(toolCall.Function.Arguments), &args)
name, _ := args["name"].(string)
surname, _ := args["surname"].(string)
birthYear, _ := args["birth_year"].(float64)
log.Printf(" │ → API call: get_access_level(%s, %s, %d)", name, surname, int(birthYear))
req := domain.AccessLevelRequest{
APIKey: uc.apiKey,
Name: name,
Surname: surname,
BirthYear: int(birthYear),
}
response, err := uc.apiClient.GetAccessLevel(ctx, req)
if err != nil {
return 0, fmt.Errorf("API call failed: %w", err)
}
// Save to file
fileName := fmt.Sprintf("%s_%s.json", name, surname)
filePath := filepath.Join(uc.outputDir, "accesslevel", fileName)
os.WriteFile(filePath, response, 0644)
// Parse access level
var accessData struct {
AccessLevel int `json:"accessLevel"`
}
if err := json.Unmarshal(response, &accessData); err != nil {
return 0, fmt.Errorf("parsing access level: %w", err)
}
return accessData.AccessLevel, nil
}
// loadPowerPlantsData loads power plants data from API
func (uc *PersonAgentProcessorUseCase) loadPowerPlantsData(ctx context.Context) (*domain.PowerPlantsData, error) {
url := fmt.Sprintf("https://hub.ag3nts.org/data/%s/findhim_locations.json", uc.apiKey)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var plantsData domain.PowerPlantsData
if err := json.Unmarshal(body, &plantsData); err != nil {
return nil, err
}
return &plantsData, nil
}
// fetchPlantGeolocations fetches accurate coordinates for all power plants using geocoding API
func (uc *PersonAgentProcessorUseCase) fetchPlantGeolocations(
ctx context.Context,
plantsData *domain.PowerPlantsData,
) (map[string]struct{ Lat, Lon float64 }, error) {
coordinates := make(map[string]struct{ Lat, Lon float64 })
for city := range plantsData.PowerPlants {
log.Printf(" • Geocoding: %s...", city)
lat, lon, err := domain.GetPlantGeolocation(ctx, city)
if err != nil {
log.Printf(" ✗ Error: %v (using fallback)", err)
// Use fallback coordinates if available
if fallback, ok := domain.CityCoordinates[city]; ok {
coordinates[city] = fallback
log.Printf(" → Using fallback: %.4f°N, %.4f°E", fallback.Lat, fallback.Lon)
}
continue
}
coordinates[city] = struct{ Lat, Lon float64 }{Lat: lat, Lon: lon}
log.Printf(" ✓ Fetched: %.6f°N, %.6f°E", lat, lon)
}
return coordinates, nil
}
// executeToolCall handles execution of various tool calls
func (uc *PersonAgentProcessorUseCase) executeToolCall(
ctx context.Context,
toolCall domain.ToolCall,
) (string, error) {
var args map[string]interface{}
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
return "", fmt.Errorf("parsing arguments: %w", err)
}
switch toolCall.Function.Name {
case "get_location":
name, _ := args["name"].(string)
surname, _ := args["surname"].(string)
log.Printf(" │ → Tool call: get_location(%s, %s)", name, surname)
req := domain.LocationRequest{
APIKey: uc.apiKey,
Name: name,
Surname: surname,
}
response, err := uc.apiClient.GetLocation(ctx, req)
if err != nil {
return fmt.Sprintf(`{"error": "%v"}`, err), nil
}
// Save to file
fileName := fmt.Sprintf("%s_%s.json", name, surname)
filePath := filepath.Join(uc.outputDir, "locations", fileName)
os.WriteFile(filePath, response, 0644)
return string(response), nil
case "get_access_level":
name, _ := args["name"].(string)
surname, _ := args["surname"].(string)
birthYear, _ := args["birth_year"].(float64)
log.Printf(" │ → Tool call: get_access_level(%s, %s, %d)", name, surname, int(birthYear))
req := domain.AccessLevelRequest{
APIKey: uc.apiKey,
Name: name,
Surname: surname,
BirthYear: int(birthYear),
}
response, err := uc.apiClient.GetAccessLevel(ctx, req)
if err != nil {
return fmt.Sprintf(`{"error": "%v"}`, err), nil
}
// Save to file
fileName := fmt.Sprintf("%s_%s.json", name, surname)
filePath := filepath.Join(uc.outputDir, "accesslevel", fileName)
os.WriteFile(filePath, response, 0644)
return string(response), nil
case "find_nearest_point":
referenceLat, _ := args["reference_lat"].(float64)
referenceLon, _ := args["reference_lon"].(float64)
pointsRaw, _ := args["points"].([]interface{})
log.Printf(" │ → Tool call: find_nearest_point(ref: %.4f,%.4f, %d points)", referenceLat, referenceLon, len(pointsRaw))
// Parse points array
var points [][]float64
for _, p := range pointsRaw {
if pointArr, ok := p.([]interface{}); ok && len(pointArr) >= 2 {
lat, _ := pointArr[0].(float64)
lon, _ := pointArr[1].(float64)
points = append(points, []float64{lat, lon})
}
}
result := domain.FindNearestPoint(referenceLat, referenceLon, points)
if result == nil {
return `{"error": "no valid points provided"}`, nil
}
log.Printf(" │ → Result: nearest point at index %d, distance %.2f km", result.Index, result.DistanceKm)
resultJSON, _ := json.Marshal(result)
return string(resultJSON), nil
default:
return fmt.Sprintf(`{"error": "unknown function: %s"}`, toolCall.Function.Name), nil
}
}

View File

@@ -0,0 +1,118 @@
package usecase
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"github.com/paramah/ai_devs4/s01e02/internal/domain"
)
// ProcessPersonsUseCase handles the processing of persons
type ProcessPersonsUseCase struct {
personRepo domain.PersonRepository
apiClient domain.APIClient
apiKey string
outputDir string
}
// NewProcessPersonsUseCase creates a new use case instance
func NewProcessPersonsUseCase(
personRepo domain.PersonRepository,
apiClient domain.APIClient,
apiKey string,
outputDir string,
) *ProcessPersonsUseCase {
return &ProcessPersonsUseCase{
personRepo: personRepo,
apiClient: apiClient,
apiKey: apiKey,
outputDir: outputDir,
}
}
// Execute processes all persons from the input file
func (uc *ProcessPersonsUseCase) Execute(ctx context.Context, inputFile string) error {
// Load persons from file
log.Printf("Loading persons from: %s", inputFile)
persons, err := uc.personRepo.LoadPersons(ctx, inputFile)
if err != nil {
return fmt.Errorf("loading persons: %w", err)
}
log.Printf("Loaded %d persons", len(persons))
// Process each person
for i, person := range persons {
log.Printf("\n[%d/%d] Processing: %s %s", i+1, len(persons), person.Name, person.Surname)
// Get location information
if err := uc.processLocation(ctx, person); err != nil {
log.Printf("Error getting location for %s %s: %v", person.Name, person.Surname, err)
continue
}
// Get access level information
if err := uc.processAccessLevel(ctx, person); err != nil {
log.Printf("Error getting access level for %s %s: %v", person.Name, person.Surname, err)
continue
}
}
log.Printf("\nProcessing completed!")
return nil
}
// processLocation gets and saves location information for a person
func (uc *ProcessPersonsUseCase) processLocation(ctx context.Context, person domain.Person) error {
req := domain.LocationRequest{
APIKey: uc.apiKey,
Name: person.Name,
Surname: person.Surname,
}
log.Printf(" → Getting location...")
response, err := uc.apiClient.GetLocation(ctx, req)
if err != nil {
return fmt.Errorf("getting location: %w", err)
}
// Save to file
fileName := fmt.Sprintf("%s_%s.json", person.Name, person.Surname)
filePath := filepath.Join(uc.outputDir, "locations", fileName)
if err := os.WriteFile(filePath, response, 0644); err != nil {
return fmt.Errorf("saving location response: %w", err)
}
log.Printf(" ✓ Location saved to: %s", filePath)
return nil
}
// processAccessLevel gets and saves access level information for a person
func (uc *ProcessPersonsUseCase) processAccessLevel(ctx context.Context, person domain.Person) error {
req := domain.AccessLevelRequest{
APIKey: uc.apiKey,
Name: person.Name,
Surname: person.Surname,
BirthYear: person.Born,
}
log.Printf(" → Getting access level...")
response, err := uc.apiClient.GetAccessLevel(ctx, req)
if err != nil {
return fmt.Errorf("getting access level: %w", err)
}
// Save to file
fileName := fmt.Sprintf("%s_%s.json", person.Name, person.Surname)
filePath := filepath.Join(uc.outputDir, "accesslevel", fileName)
if err := os.WriteFile(filePath, response, 0644); err != nil {
return fmt.Errorf("saving access level response: %w", err)
}
log.Printf(" ✓ Access level saved to: %s", filePath)
return nil
}

53
lista.json Normal file
View File

@@ -0,0 +1,53 @@
[
{
"name": "Cezary",
"surname": "Żurek",
"gender": "M",
"born": 1987,
"city": "Grudziądz",
"tags": [
"transport"
]
},
{
"name": "Jacek",
"surname": "Nowak",
"gender": "M",
"born": 1991,
"city": "Grudziądz",
"tags": [
"transport"
]
},
{
"name": "Oskar",
"surname": "Sieradzki",
"gender": "M",
"born": 1993,
"city": "Grudziądz",
"tags": [
"transport"
]
},
{
"name": "Wojciech",
"surname": "Bielik",
"gender": "M",
"born": 1986,
"city": "Grudziądz",
"tags": [
"transport"
]
},
{
"name": "Wacław",
"surname": "Jasiński",
"gender": "M",
"born": 1986,
"city": "Grudziądz",
"tags": [
"transport",
"praca z pojazdami"
]
}
]