final
This commit is contained in:
239
internal/usecase/agent_processor.go
Normal file
239
internal/usecase/agent_processor.go
Normal 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
|
||||
}
|
||||
}
|
||||
185
internal/usecase/find_closest_agent.go
Normal file
185
internal/usecase/find_closest_agent.go
Normal 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)
|
||||
}
|
||||
}
|
||||
461
internal/usecase/optimized_agent_processor.go
Normal file
461
internal/usecase/optimized_agent_processor.go
Normal 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
|
||||
}
|
||||
694
internal/usecase/person_agent_processor.go
Normal file
694
internal/usecase/person_agent_processor.go
Normal 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
|
||||
}
|
||||
}
|
||||
118
internal/usecase/process_persons.go
Normal file
118
internal/usecase/process_persons.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user