Files
s01e02/internal/usecase/person_agent_processor.go
2026-03-12 02:10:57 +01:00

695 lines
24 KiB
Go

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
}
}