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