final
This commit is contained in:
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Binaries
|
||||||
|
person-processor
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
# Config with secrets
|
||||||
|
config.json
|
||||||
|
|
||||||
|
# Output directories
|
||||||
|
output/
|
||||||
19
CLAUDE.md
Normal file
19
CLAUDE.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Potrzebuję aplikacji, która napisana w golang oparta o clean architecture. Będziemy koszystali z LLM, wykorzystaj sposób łączenia z llm taki sam jak w projekcie który jest dostępny w katalogu ../s01e01/ przenieś potrzebne elementy z tamtej aplikacji. Nasza aplikacja zrealizuje zadanie:
|
||||||
|
|
||||||
|
1. w pliku lista.json mamy dane osób, które musimy pobrać do dalszej pracy
|
||||||
|
2. Wysyłamy na endpoint za pomocą POST w raw json zapytanie dla każdej z osób na liście w takiej formie
|
||||||
|
{
|
||||||
|
"apikey": "tutaj-twój-klucz",
|
||||||
|
"name": "Jan",
|
||||||
|
"surname": "Kowalski"
|
||||||
|
}
|
||||||
|
3. odpowiedzi zapisujemy w plikach json w katalogu outout/locations/
|
||||||
|
4. na endpoint https://hub.ag3nts.org/api/accesslevel metodą POST w raw json wysyłamy zapytanie dla każdej z osób w formie
|
||||||
|
{
|
||||||
|
"apikey": "tutaj-twój-klucz",
|
||||||
|
"name": "Jan",
|
||||||
|
"surname": "Kowalski",
|
||||||
|
"birthYear": 1987
|
||||||
|
}
|
||||||
|
4. odpowiedzi zapisujemy w plikach json w katalogu output/accesslevel
|
||||||
|
|
||||||
365
README.md
Normal file
365
README.md
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
# AI Agent Person Processor ⚡
|
||||||
|
|
||||||
|
Zaawansowana aplikacja w Go oparta o Clean Architecture, która wykorzystuje **ZOPTYMALIZOWANY** proces z LLM Function Calling + równoległymi obliczeniami do inteligentnego przetwarzania danych osób i generowania raportów lokalizacyjnych.
|
||||||
|
|
||||||
|
## 🚀 Kluczowa optymalizacja:
|
||||||
|
- **Tylko 2 wywołania LLM na osobę** (poprzednio: 30-50)
|
||||||
|
- **Równoległe obliczenia odległości** (wszystkie elektrownie jednocześnie)
|
||||||
|
- **20x szybsze** przetwarzanie
|
||||||
|
- **Wielowątkowe goroutines** w Go
|
||||||
|
|
||||||
|
## Funkcjonalność
|
||||||
|
|
||||||
|
### Zoptymalizowany proces przetwarzania:
|
||||||
|
|
||||||
|
**Dla każdej osoby sekwencyjnie - TYLKO 2 WYWOŁANIA LLM:**
|
||||||
|
|
||||||
|
1. **[LLM Call 1/2] Pobierz lokalizacje osoby** (function calling: `get_location`)
|
||||||
|
- Zwraca **WSZYSTKIE** możliwe lokalizacje osoby (np. 7, 12, 17 lokalizacji)
|
||||||
|
|
||||||
|
2. **[Lokalne obliczenia RÓWNOLEGŁE] Znajdź globalnie najbliższą elektrownię**
|
||||||
|
- **Dla KAŻDEJ lokalizacji osoby** (pętla):
|
||||||
|
- Dla KAŻDEJ elektrowni uruchamia oddzielną goroutine
|
||||||
|
- Każda goroutine pobiera współrzędne z geocoding cache
|
||||||
|
- Równolegle oblicza odległość Haversine (bez LLM!)
|
||||||
|
- Znajduje najbliższą elektrownię dla tej konkretnej lokalizacji
|
||||||
|
- **Wybiera MINIMUM ze wszystkich lokalizacji×elektrowni**
|
||||||
|
- **Wielowątkowe przetwarzanie** - wszystkie elektrownie jednocześnie
|
||||||
|
|
||||||
|
3. **[LLM Call 2/2] Pobierz poziom dostępu** (function calling: `get_access_level`)
|
||||||
|
- Używa tylko roku urodzenia (integer)
|
||||||
|
|
||||||
|
4. **Generuje raport dla osoby**
|
||||||
|
- Zapisuje w `output/person_reports/{Name}_{Surname}.json`
|
||||||
|
- Zawiera: nazwę elektrowni, kod, odległość, poziom dostępu, współrzędne
|
||||||
|
|
||||||
|
**Po przetworzeniu wszystkich osób:**
|
||||||
|
|
||||||
|
- Znajduje osobę z **najmniejszą odległością** do jej najbliższej elektrowni
|
||||||
|
- Generuje `output/final_answer.json` do weryfikacji
|
||||||
|
|
||||||
|
## Architektura
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── cmd/app/ # Punkt wejścia
|
||||||
|
├── internal/
|
||||||
|
│ ├── config/ # Konfiguracja
|
||||||
|
│ ├── domain/ # Modele i interfejsy
|
||||||
|
│ │ ├── person.go # Model osoby
|
||||||
|
│ │ ├── location.go # Haversine i obliczenia odległości
|
||||||
|
│ │ ├── report.go # Modele raportów
|
||||||
|
│ │ ├── llm.go # LLM z function calling
|
||||||
|
│ │ └── tools.go # Definicje narzędzi dla agenta
|
||||||
|
│ ├── infrastructure/
|
||||||
|
│ │ ├── api/ # Klient API hub.ag3nts.org
|
||||||
|
│ │ ├── json/ # Repozytoria JSON
|
||||||
|
│ │ └── llm/ # Providery LLM
|
||||||
|
│ └── usecase/
|
||||||
|
│ └── person_agent_processor.go # Logika przetwarzania osób przez agenta
|
||||||
|
├── output/
|
||||||
|
│ ├── locations/ # Dane lokalizacji osób (z API)
|
||||||
|
│ ├── accesslevel/ # Dane poziomów dostępu (z API)
|
||||||
|
│ ├── person_reports/ # Raport dla każdej osoby
|
||||||
|
│ ├── findhim_locations.json # Lista elektrowni z kodami
|
||||||
|
│ └── final_answer.json # Końcowa odpowiedź do weryfikacji
|
||||||
|
└── lista.json # Dane wejściowe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Przykładowy raport osoby
|
||||||
|
|
||||||
|
Raport dla osoby (`output/person_reports/Oskar_Sieradzki.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Oskar",
|
||||||
|
"surname": "Sieradzki",
|
||||||
|
"nearest_plant": "Grudziądz",
|
||||||
|
"plant_code": "PWR7264PL",
|
||||||
|
"distance_km": 83.38,
|
||||||
|
"access_level": 7,
|
||||||
|
"primary_latitude": 53.483,
|
||||||
|
"primary_longitude": 18.754
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Każdy raport zawiera najbliższą elektrownię dla danej osoby wraz z odległością i kodem elektrowni.
|
||||||
|
|
||||||
|
## Optymalizacje
|
||||||
|
|
||||||
|
### ⚡ EKSTREMALNA OPTYMALIZACJA - 2 WYWOŁANIA LLM NA OSOBĘ:
|
||||||
|
|
||||||
|
**Poprzednia wersja:** 30-50 iteracji LLM na osobę
|
||||||
|
**Nowa wersja:** **TYLKO 2 wywołania LLM na osobę** ✨
|
||||||
|
|
||||||
|
### Jak to działa:
|
||||||
|
|
||||||
|
**FAZA INICJALIZACJI (jednokrotnie przy starcie):**
|
||||||
|
- Pobiera listę elektrowni z API
|
||||||
|
- **Geocoding API (OpenStreetMap Nominatim):** dla każdej elektrowni pobiera dokładne współrzędne
|
||||||
|
- Zapisuje współrzędne w pamięci cache
|
||||||
|
- Fallback do `CityCoordinates` jeśli API nie odpowiada
|
||||||
|
|
||||||
|
**FAZA PRZETWARZANIA (dla każdej osoby):**
|
||||||
|
1. **LLM Call 1:** `get_location` - pobiera **WSZYSTKIE** lokalizacje osoby (function calling)
|
||||||
|
2. **Lokalne obliczenia (RÓWNOLEGŁE dla WSZYSTKICH lokalizacji):**
|
||||||
|
- **Dla KAŻDEJ lokalizacji osoby:**
|
||||||
|
- Dla każdej elektrowni: osobna goroutine
|
||||||
|
- Pobiera współrzędne z cache (dokładne z geocoding API!)
|
||||||
|
- Oblicza odległość Haversine
|
||||||
|
- Znajduje najbliższą elektrownię dla tej lokalizacji
|
||||||
|
- **Wybiera GLOBALNIE najmniejszą odległość** ze wszystkich kombinacji
|
||||||
|
- Wszystko dzieje się **jednocześnie** (wielowątkowo)
|
||||||
|
- **BEZ użycia LLM!**
|
||||||
|
3. **LLM Call 2:** `get_access_level` - pobiera poziom dostępu (function calling)
|
||||||
|
|
||||||
|
### Kluczowe ulepszenia:
|
||||||
|
- **Redukcja wywołań LLM z ~40 do 2** (20x szybciej! 💨)
|
||||||
|
- **Równoległe przetwarzanie** - wszystkie elektrownie jednocześnie
|
||||||
|
- **Dokładne współrzędne** - OpenStreetMap Nominatim API (geocoding)
|
||||||
|
- **Geocoding przy starcie** - jednokrotne pobranie współrzędnych wszystkich elektrowni
|
||||||
|
- **Haversine bez LLM** - lokalne obliczenia w Go
|
||||||
|
- **Fallback coordinates** - jeśli geocoding API zawiedzie, używamy CityCoordinates
|
||||||
|
- Temperature = 0.0 dla deterministycznych wyników
|
||||||
|
|
||||||
|
### Zalety tego podejścia:
|
||||||
|
- ⚡ **Dramatycznie szybsze** - 20x mniej wywołań LLM
|
||||||
|
- 💰 **Tańsze** - minimalna konsumpcja API
|
||||||
|
- 🚀 **Równoległe obliczenia** - wykorzystanie wszystkich rdzeni CPU
|
||||||
|
- 🎯 **Deterministyczne** - te same wyniki za każdym razem
|
||||||
|
- 📊 **Szczegółowe logi** - każdy krok widoczny
|
||||||
|
- 🔧 **Łatwe debugowanie** - jasna struktura 2-fazowa
|
||||||
|
|
||||||
|
### System logowania:
|
||||||
|
Aplikacja generuje bardzo szczegółowe logi pokazujące:
|
||||||
|
- **Fazę ładowania danych** (osoby, elektrownie)
|
||||||
|
- **Dla każdej osoby:**
|
||||||
|
- `[LLM Call 1/2]` - wywołanie get_location
|
||||||
|
- `[Local Processing]` - równoległe goroutines dla każdej elektrowni
|
||||||
|
- Wyniki z każdej goroutine: `Goroutine [Zabrze]: 123.45 km`
|
||||||
|
- `[LLM Call 2/2]` - wywołanie get_access_level
|
||||||
|
- Finalne wyniki z podsumowaniem `Total LLM calls: 2`
|
||||||
|
- **Fazę znajdowania minimalnej odległości**
|
||||||
|
- **Ostateczny wynik** z instrukcją weryfikacji
|
||||||
|
|
||||||
|
## Konfiguracja
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"api_key": "your-api-key",
|
||||||
|
"input_file": "lista.json",
|
||||||
|
"output_dir": "output",
|
||||||
|
"locations_api": "https://hub.ag3nts.org/api/location",
|
||||||
|
"access_level_api": "https://hub.ag3nts.org/api/accesslevel",
|
||||||
|
"locations_url": "https://hub.ag3nts.org",
|
||||||
|
"llm": {
|
||||||
|
"provider": "lmstudio",
|
||||||
|
"model": "bielik-11b-v3.0-instruct-gptq-marlin@q8_0",
|
||||||
|
"base_url": "http://localhost:1234"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Providery LLM
|
||||||
|
|
||||||
|
#### LM Studio (lokalny)
|
||||||
|
```json
|
||||||
|
"llm": {
|
||||||
|
"provider": "lmstudio",
|
||||||
|
"model": "bielik-11b-v3.0-instruct-gptq-marlin@q8_0",
|
||||||
|
"base_url": "http://localhost:1234"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### OpenRouter (API)
|
||||||
|
```json
|
||||||
|
"llm": {
|
||||||
|
"provider": "openrouter",
|
||||||
|
"model": "anthropic/claude-3.5-sonnet",
|
||||||
|
"api_key": "your-openrouter-api-key"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Uwaga**: Model Bielik jest zalecany dla function calling - działa szybciej i stabilniej niż DeepSeek R1.
|
||||||
|
|
||||||
|
## Budowanie i uruchomienie
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Budowanie
|
||||||
|
go build -o person-processor ./cmd/app
|
||||||
|
|
||||||
|
# Uruchomienie
|
||||||
|
./person-processor -config config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Weryfikacja odpowiedzi
|
||||||
|
|
||||||
|
Po zakończeniu przetwarzania, aplikacja zapisuje końcową odpowiedź w pliku `output/final_answer.json`.
|
||||||
|
|
||||||
|
Aby wysłać odpowiedź do weryfikacji:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://hub.ag3nts.org/verify \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @output/final_answer.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Lub z formatowaniem odpowiedzi:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://hub.ag3nts.org/verify \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @output/final_answer.json | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
Format odpowiedzi (`output/final_answer.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"apikey": "your-api-key",
|
||||||
|
"task": "findhim",
|
||||||
|
"answer": {
|
||||||
|
"name": "Imię",
|
||||||
|
"surname": "Nazwisko",
|
||||||
|
"accessLevel": 7,
|
||||||
|
"powerPlant": "PWR1234PL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wzór Haversine
|
||||||
|
|
||||||
|
Aplikacja używa wzoru Haversine do obliczania odległości między dwoma punktami na kuli ziemskiej:
|
||||||
|
|
||||||
|
```
|
||||||
|
a = sin²(Δlat/2) + cos(lat1) × cos(lat2) × sin²(Δlon/2)
|
||||||
|
c = 2 × atan2(√a, √(1−a))
|
||||||
|
distance = R × c
|
||||||
|
```
|
||||||
|
|
||||||
|
gdzie R = 6371 km (promień Ziemi)
|
||||||
|
|
||||||
|
## Narzędzia agenta LLM i lokalne funkcje
|
||||||
|
|
||||||
|
### 🤖 LLM Function Calling (2 wywołania na osobę):
|
||||||
|
|
||||||
|
#### get_location
|
||||||
|
Pobiera wszystkie możliwe lokalizacje osoby z API.
|
||||||
|
|
||||||
|
**Parametry:**
|
||||||
|
- `name` (string) - imię osoby
|
||||||
|
- `surname` (string) - nazwisko osoby
|
||||||
|
|
||||||
|
**Zwraca:** tablicę obiektów z `latitude` i `longitude`
|
||||||
|
|
||||||
|
**Logi:** `→ API call: get_location(Imię, Nazwisko)`
|
||||||
|
|
||||||
|
**Użycie:** LLM Call 1/2
|
||||||
|
|
||||||
|
#### get_access_level
|
||||||
|
Pobiera poziom dostępu osoby z API.
|
||||||
|
|
||||||
|
**Parametry:**
|
||||||
|
- `name` (string) - imię osoby
|
||||||
|
- `surname` (string) - nazwisko osoby
|
||||||
|
- `birth_year` (integer) - **tylko rok** urodzenia (np. 1987, NIE pełna data)
|
||||||
|
|
||||||
|
**Zwraca:** obiekt z `accessLevel` (integer)
|
||||||
|
|
||||||
|
**Logi:** `→ API call: get_access_level(Imię, Nazwisko, rok)`
|
||||||
|
|
||||||
|
**Użycie:** LLM Call 2/2
|
||||||
|
|
||||||
|
#### find_nearest_point
|
||||||
|
Znajduje najbliższy punkt z listy do punktu referencyjnego (np. elektrownia).
|
||||||
|
|
||||||
|
**Parametry:**
|
||||||
|
- `reference_lat` (float) - szerokość geograficzna punktu referencyjnego
|
||||||
|
- `reference_lon` (float) - długość geograficzna punktu referencyjnego
|
||||||
|
- `points` (array) - tablica punktów do sprawdzenia, każdy punkt: `[latitude, longitude]`
|
||||||
|
|
||||||
|
**Zwraca:** obiekt z `latitude`, `longitude`, `distance_km`, `index` (indeks najbliższego punktu)
|
||||||
|
|
||||||
|
**Przykład wywołania:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"reference_lat": 50.3086,
|
||||||
|
"reference_lon": 18.7864,
|
||||||
|
"points": [
|
||||||
|
[52.2297, 21.0122],
|
||||||
|
[51.1079, 17.0385],
|
||||||
|
[50.0647, 19.9450]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zwraca:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"latitude": 50.0647,
|
||||||
|
"longitude": 19.9450,
|
||||||
|
"distance_km": 45.23,
|
||||||
|
"index": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Logi:** `→ Tool call: find_nearest_point(ref: 50.3086,18.7864, 3 points)`
|
||||||
|
|
||||||
|
**Użycie:** Opcjonalna - dostępna dla LLM jeśli zdecyduje się jej użyć
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ⚡ Lokalne funkcje (bez LLM - wielowątkowe):
|
||||||
|
|
||||||
|
#### GetPlantGeolocation (Geocoding API)
|
||||||
|
Pobiera dokładne współrzędne geograficzne elektrowni z OpenStreetMap Nominatim API.
|
||||||
|
|
||||||
|
**API:** `https://nominatim.openstreetmap.org/search`
|
||||||
|
|
||||||
|
**Parametry:**
|
||||||
|
- `city` - nazwa miasta
|
||||||
|
- `country` - Poland
|
||||||
|
- `format` - json
|
||||||
|
- `limit` - 1
|
||||||
|
|
||||||
|
**Zwraca:** Dokładne współrzędne `{lat, lon}` (6 miejsc po przecinku!)
|
||||||
|
|
||||||
|
**Użycie:** Jednokrotnie przy starcie aplikacji (cache w pamięci)
|
||||||
|
|
||||||
|
**Logi:**
|
||||||
|
```
|
||||||
|
→ Fetching accurate geolocations from geocoding API...
|
||||||
|
• Geocoding: Zabrze...
|
||||||
|
✓ Fetched: 50.308615°N, 18.786375°E
|
||||||
|
• Geocoding: Grudziądz...
|
||||||
|
✓ Fetched: 53.483624°N, 18.753536°E
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CityCoordinates (baza fallback)
|
||||||
|
Hardcoded współrzędne wszystkich elektrowni w Polsce (fallback gdy API nie odpowiada).
|
||||||
|
|
||||||
|
**Zawartość:**
|
||||||
|
- Zabrze, Piotrków Trybunalski, Grudziądz, Tczew, Radom, Chełmno, Żarnowiec
|
||||||
|
- Każde miasto: `{Lat: float64, Lon: float64}`
|
||||||
|
|
||||||
|
**Użycie:** Fallback gdy geocoding API zawiedzie
|
||||||
|
|
||||||
|
#### Haversine (wzór odległości)
|
||||||
|
Oblicza odległość między dwoma punktami na kuli ziemskiej.
|
||||||
|
|
||||||
|
**Parametry:**
|
||||||
|
- `lat1, lon1` - współrzędne punktu 1
|
||||||
|
- `lat2, lon2` - współrzędne punktu 2
|
||||||
|
|
||||||
|
**Zwraca:** odległość w kilometrach (float64)
|
||||||
|
|
||||||
|
**Użycie:** Równoległe goroutines dla każdej elektrowni (bez LLM!)
|
||||||
|
|
||||||
|
**Logi:** `• Goroutine [Nazwa]: 123.45 km`
|
||||||
|
|
||||||
|
|
||||||
|
## Wymagania
|
||||||
|
|
||||||
|
- Go 1.21+
|
||||||
|
- LM Studio lub klucz OpenRouter API
|
||||||
|
- Model LLM wspierający function calling (zalecany: Bielik 11B)
|
||||||
73
cmd/app/main.go
Normal file
73
cmd/app/main.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/paramah/ai_devs4/s01e02/internal/config"
|
||||||
|
"github.com/paramah/ai_devs4/s01e02/internal/domain"
|
||||||
|
"github.com/paramah/ai_devs4/s01e02/internal/infrastructure/api"
|
||||||
|
"github.com/paramah/ai_devs4/s01e02/internal/infrastructure/json"
|
||||||
|
"github.com/paramah/ai_devs4/s01e02/internal/infrastructure/llm"
|
||||||
|
"github.com/paramah/ai_devs4/s01e02/internal/usecase"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
configPath := flag.String("config", "config.json", "Path to configuration file")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
cfg, err := config.Load(*configPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load configuration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfg.Validate(); err != nil {
|
||||||
|
log.Fatalf("Invalid configuration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("========== AI Agent Person Processor ==========")
|
||||||
|
log.Printf("Input file: %s", cfg.InputFile)
|
||||||
|
log.Printf("Output directory: %s", cfg.OutputDir)
|
||||||
|
log.Printf("LLM Provider: %s", cfg.LLM.Provider)
|
||||||
|
log.Printf("LLM Model: %s", cfg.LLM.Model)
|
||||||
|
log.Printf("===============================================\n")
|
||||||
|
|
||||||
|
// Create repositories and clients
|
||||||
|
personRepo := json.NewRepository()
|
||||||
|
apiClient := api.NewClient(cfg.LocationsAPI, cfg.AccessLevelAPI)
|
||||||
|
|
||||||
|
// Create LLM provider
|
||||||
|
var llmProvider domain.LLMProvider
|
||||||
|
switch cfg.LLM.Provider {
|
||||||
|
case "openrouter":
|
||||||
|
llmProvider = llm.NewOpenRouterProvider(cfg.LLM.APIKey, cfg.LLM.Model)
|
||||||
|
log.Printf("Using OpenRouter with model: %s", cfg.LLM.Model)
|
||||||
|
case "lmstudio":
|
||||||
|
llmProvider = llm.NewLMStudioProvider(cfg.LLM.BaseURL, cfg.LLM.Model)
|
||||||
|
log.Printf("Using LM Studio at %s with model: %s", cfg.LLM.BaseURL, cfg.LLM.Model)
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown LLM provider: %s", cfg.LLM.Provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create person agent processor use case
|
||||||
|
personAgentUC := usecase.NewPersonAgentProcessorUseCase(
|
||||||
|
personRepo,
|
||||||
|
apiClient,
|
||||||
|
llmProvider,
|
||||||
|
cfg.APIKey,
|
||||||
|
cfg.OutputDir,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute processing
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := personAgentUC.Execute(ctx, cfg.InputFile); err != nil {
|
||||||
|
log.Fatalf("Failed to process persons: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("\n========== All tasks completed successfully! ==========")
|
||||||
|
}
|
||||||
7
config.example.json
Normal file
7
config.example.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"api_key": "YOUR_API_KEY_HERE",
|
||||||
|
"input_file": "lista.json",
|
||||||
|
"output_dir": "output",
|
||||||
|
"locations_api": "https://hub.ag3nts.org/api/locations",
|
||||||
|
"access_level_api": "https://hub.ag3nts.org/api/accesslevel"
|
||||||
|
}
|
||||||
86
internal/config/config.go
Normal file
86
internal/config/config.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the application configuration
|
||||||
|
type Config struct {
|
||||||
|
APIKey string `json:"api_key"`
|
||||||
|
InputFile string `json:"input_file"`
|
||||||
|
OutputDir string `json:"output_dir"`
|
||||||
|
LocationsAPI string `json:"locations_api"`
|
||||||
|
AccessLevelAPI string `json:"access_level_api"`
|
||||||
|
LocationsURL string `json:"locations_url"`
|
||||||
|
LLM LLMConfig `json:"llm"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLMConfig contains configuration for LLM provider
|
||||||
|
type LLMConfig struct {
|
||||||
|
Provider string `json:"provider"` // "openrouter" or "lmstudio"
|
||||||
|
Model string `json:"model"`
|
||||||
|
APIKey string `json:"api_key,omitempty"` // For OpenRouter
|
||||||
|
BaseURL string `json:"base_url,omitempty"` // For LM Studio
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load loads configuration from a JSON file
|
||||||
|
func Load(path string) (*Config, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the configuration
|
||||||
|
func (c *Config) Validate() error {
|
||||||
|
if c.APIKey == "" {
|
||||||
|
return fmt.Errorf("api_key is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.InputFile == "" {
|
||||||
|
return fmt.Errorf("input_file is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.OutputDir == "" {
|
||||||
|
return fmt.Errorf("output_dir is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.LocationsAPI == "" {
|
||||||
|
return fmt.Errorf("locations_api is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.AccessLevelAPI == "" {
|
||||||
|
return fmt.Errorf("access_level_api is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.LocationsURL == "" {
|
||||||
|
return fmt.Errorf("locations_url is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.LLM.Provider != "openrouter" && c.LLM.Provider != "lmstudio" {
|
||||||
|
return fmt.Errorf("llm.provider must be 'openrouter' or 'lmstudio'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.LLM.Model == "" {
|
||||||
|
return fmt.Errorf("llm.model is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.LLM.Provider == "openrouter" && c.LLM.APIKey == "" {
|
||||||
|
return fmt.Errorf("llm.api_key is required for openrouter provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.LLM.Provider == "lmstudio" && c.LLM.BaseURL == "" {
|
||||||
|
return fmt.Errorf("llm.base_url is required for lmstudio provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
24
internal/domain/api_client.go
Normal file
24
internal/domain/api_client.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// LocationRequest represents a request for location information
|
||||||
|
type LocationRequest struct {
|
||||||
|
APIKey string `json:"apikey"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Surname string `json:"surname"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccessLevelRequest represents a request for access level information
|
||||||
|
type AccessLevelRequest struct {
|
||||||
|
APIKey string `json:"apikey"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Surname string `json:"surname"`
|
||||||
|
BirthYear int `json:"birthYear"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIClient defines the interface for API operations
|
||||||
|
type APIClient interface {
|
||||||
|
GetLocation(ctx context.Context, req LocationRequest) ([]byte, error)
|
||||||
|
GetAccessLevel(ctx context.Context, req AccessLevelRequest) ([]byte, error)
|
||||||
|
}
|
||||||
50
internal/domain/city_coordinates.go
Normal file
50
internal/domain/city_coordinates.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
// CityCoordinates contains coordinates for Polish cities with power plants
|
||||||
|
var CityCoordinates = map[string]struct{ Lat, Lon float64 }{
|
||||||
|
"Zabrze": {Lat: 50.3015, Lon: 18.7912},
|
||||||
|
"Piotrków Trybunalski": {Lat: 51.4054, Lon: 19.7031},
|
||||||
|
"Grudziądz": {Lat: 53.4836, Lon: 18.7536},
|
||||||
|
"Tczew": {Lat: 54.0915, Lon: 18.7793},
|
||||||
|
"Radom": {Lat: 51.4027, Lon: 21.1471},
|
||||||
|
"Chelmno": {Lat: 53.3479, Lon: 18.4256},
|
||||||
|
"Żarnowiec": {Lat: 54.7317, Lon: 18.0431},
|
||||||
|
}
|
||||||
|
|
||||||
|
// PowerPlantInfo contains information about a power plant
|
||||||
|
type PowerPlantInfo struct {
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
Power string `json:"power"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PowerPlantsData represents the structure from findhim_locations.json
|
||||||
|
type PowerPlantsData struct {
|
||||||
|
PowerPlants map[string]PowerPlantInfo `json:"power_plants"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToLocations converts power plants data to locations with coordinates
|
||||||
|
func (p *PowerPlantsData) ToLocations() []Location {
|
||||||
|
var locations []Location
|
||||||
|
|
||||||
|
for city, info := range p.PowerPlants {
|
||||||
|
// Only include active power plants
|
||||||
|
if !info.IsActive {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
coords, ok := CityCoordinates[city]
|
||||||
|
if !ok {
|
||||||
|
// Skip cities without known coordinates
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
locations = append(locations, Location{
|
||||||
|
Name: city,
|
||||||
|
Latitude: coords.Lat,
|
||||||
|
Longitude: coords.Lon,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return locations
|
||||||
|
}
|
||||||
66
internal/domain/final_answer.go
Normal file
66
internal/domain/final_answer.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
// FinalAnswer represents the final answer for the task
|
||||||
|
type FinalAnswer struct {
|
||||||
|
APIKey string `json:"apikey"`
|
||||||
|
Task string `json:"task"`
|
||||||
|
Answer AnswerDetail `json:"answer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnswerDetail contains the person and power plant details
|
||||||
|
type AnswerDetail struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Surname string `json:"surname"`
|
||||||
|
AccessLevel int `json:"accessLevel"`
|
||||||
|
PowerPlant string `json:"powerPlant"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindClosestPowerPlant finds the power plant closest to any person
|
||||||
|
// Returns the person, power plant, and distance
|
||||||
|
func FindClosestPowerPlant(personDataMap map[string]*PersonData, powerPlants []Location, plantsData *PowerPlantsData) (*PersonData, *Location, float64, string) {
|
||||||
|
var closestPerson *PersonData
|
||||||
|
var closestPlant *Location
|
||||||
|
minDistance := 1e10
|
||||||
|
var powerPlantCode string
|
||||||
|
|
||||||
|
for _, personData := range personDataMap {
|
||||||
|
if len(personData.Locations) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use first location (primary)
|
||||||
|
personLoc := personData.Locations[0]
|
||||||
|
|
||||||
|
for _, plant := range powerPlants {
|
||||||
|
distance := Haversine(
|
||||||
|
personLoc.Latitude,
|
||||||
|
personLoc.Longitude,
|
||||||
|
plant.Latitude,
|
||||||
|
plant.Longitude,
|
||||||
|
)
|
||||||
|
|
||||||
|
// If distance is smaller, or same distance but higher access level
|
||||||
|
if distance < minDistance {
|
||||||
|
minDistance = distance
|
||||||
|
closestPerson = personData
|
||||||
|
closestPlant = &plant
|
||||||
|
|
||||||
|
// Get power plant code from plantsData
|
||||||
|
if info, ok := plantsData.PowerPlants[plant.Name]; ok {
|
||||||
|
powerPlantCode = info.Code
|
||||||
|
}
|
||||||
|
} else if distance == minDistance && closestPerson != nil && personData.AccessLevel > closestPerson.AccessLevel {
|
||||||
|
// Same distance, but higher access level
|
||||||
|
closestPerson = personData
|
||||||
|
closestPlant = &plant
|
||||||
|
|
||||||
|
// Get power plant code from plantsData
|
||||||
|
if info, ok := plantsData.PowerPlants[plant.Name]; ok {
|
||||||
|
powerPlantCode = info.Code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return closestPerson, closestPlant, minDistance, powerPlantCode
|
||||||
|
}
|
||||||
56
internal/domain/llm.go
Normal file
56
internal/domain/llm.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// LLMMessage represents a message in the conversation
|
||||||
|
type LLMMessage struct {
|
||||||
|
Role string `json:"role"`
|
||||||
|
Content string `json:"content,omitempty"`
|
||||||
|
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||||
|
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToolCall represents a function call from the LLM
|
||||||
|
type ToolCall struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Function Function `json:"function"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function represents the function being called
|
||||||
|
type Function struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Arguments string `json:"arguments"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tool represents a tool definition for function calling
|
||||||
|
type Tool struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Function FunctionDef `json:"function"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FunctionDef defines a function that can be called
|
||||||
|
type FunctionDef struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Parameters interface{} `json:"parameters"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLMRequest represents a request to the LLM with function calling support
|
||||||
|
type LLMRequest struct {
|
||||||
|
Messages []LLMMessage `json:"messages"`
|
||||||
|
Tools []Tool `json:"tools,omitempty"`
|
||||||
|
ToolChoice string `json:"tool_choice,omitempty"`
|
||||||
|
Temperature float64 `json:"temperature,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLMResponse represents the response from the LLM
|
||||||
|
type LLMResponse struct {
|
||||||
|
Message LLMMessage `json:"message"`
|
||||||
|
FinishReason string `json:"finish_reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLMProvider defines the interface for LLM providers
|
||||||
|
type LLMProvider interface {
|
||||||
|
Chat(ctx context.Context, request LLMRequest) (*LLMResponse, error)
|
||||||
|
}
|
||||||
180
internal/domain/location.go
Normal file
180
internal/domain/location.go
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Location represents a location with coordinates
|
||||||
|
type Location struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Latitude float64 `json:"lat"`
|
||||||
|
Longitude float64 `json:"lon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PersonLocation represents a person's location
|
||||||
|
type PersonLocation struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Surname string `json:"surname"`
|
||||||
|
Latitude float64 `json:"lat"`
|
||||||
|
Longitude float64 `json:"lon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DistanceResult represents the distance calculation result
|
||||||
|
type DistanceResult struct {
|
||||||
|
Person string `json:"person"`
|
||||||
|
Location string `json:"location"`
|
||||||
|
DistanceKm float64 `json:"distance_km"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Haversine calculates the distance between two points on Earth (in kilometers)
|
||||||
|
// using the Haversine formula
|
||||||
|
func Haversine(lat1, lon1, lat2, lon2 float64) float64 {
|
||||||
|
const earthRadiusKm = 6371.0
|
||||||
|
|
||||||
|
// Convert degrees to radians
|
||||||
|
lat1Rad := lat1 * math.Pi / 180
|
||||||
|
lat2Rad := lat2 * math.Pi / 180
|
||||||
|
deltaLat := (lat2 - lat1) * math.Pi / 180
|
||||||
|
deltaLon := (lon2 - lon1) * math.Pi / 180
|
||||||
|
|
||||||
|
// Haversine formula
|
||||||
|
a := math.Sin(deltaLat/2)*math.Sin(deltaLat/2) +
|
||||||
|
math.Cos(lat1Rad)*math.Cos(lat2Rad)*
|
||||||
|
math.Sin(deltaLon/2)*math.Sin(deltaLon/2)
|
||||||
|
|
||||||
|
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
||||||
|
|
||||||
|
return earthRadiusKm * c
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateDistance calculates distance between person and location
|
||||||
|
func CalculateDistance(person PersonLocation, location Location) DistanceResult {
|
||||||
|
distance := Haversine(person.Latitude, person.Longitude, location.Latitude, location.Longitude)
|
||||||
|
|
||||||
|
return DistanceResult{
|
||||||
|
Person: person.Name + " " + person.Surname,
|
||||||
|
Location: location.Name,
|
||||||
|
DistanceKm: distance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindClosestLocation finds the closest location to a person
|
||||||
|
func FindClosestLocation(person PersonLocation, locations []Location) *DistanceResult {
|
||||||
|
if len(locations) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var closest *DistanceResult
|
||||||
|
minDistance := math.MaxFloat64
|
||||||
|
|
||||||
|
for _, loc := range locations {
|
||||||
|
result := CalculateDistance(person, loc)
|
||||||
|
if result.DistanceKm < minDistance {
|
||||||
|
minDistance = result.DistanceKm
|
||||||
|
closest = &result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return closest
|
||||||
|
}
|
||||||
|
|
||||||
|
// NominatimResponse represents OpenStreetMap Nominatim API response
|
||||||
|
type NominatimResponse struct {
|
||||||
|
Lat string `json:"lat"`
|
||||||
|
Lon string `json:"lon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPlantGeolocation fetches accurate coordinates for a city using geocoding API
|
||||||
|
func GetPlantGeolocation(ctx context.Context, cityName string) (float64, float64, error) {
|
||||||
|
url := fmt.Sprintf("https://nominatim.openstreetmap.org/search?city=%s&country=Poland&format=json&limit=1", cityName)
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenStreetMap requires User-Agent header
|
||||||
|
req.Header.Set("User-Agent", "PersonProcessor/1.0")
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("making request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("reading response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []NominatimResponse
|
||||||
|
if err := json.Unmarshal(body, &results); err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("parsing JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) == 0 {
|
||||||
|
return 0, 0, fmt.Errorf("no results found for city: %s", cityName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse coordinates
|
||||||
|
var lat, lon float64
|
||||||
|
if _, err := fmt.Sscanf(results[0].Lat, "%f", &lat); err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("parsing latitude: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := fmt.Sscanf(results[0].Lon, "%f", &lon); err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("parsing longitude: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lat, lon, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NearestPointResult represents result of finding nearest point
|
||||||
|
type NearestPointResult struct {
|
||||||
|
Latitude float64 `json:"latitude"`
|
||||||
|
Longitude float64 `json:"longitude"`
|
||||||
|
DistanceKm float64 `json:"distance_km"`
|
||||||
|
Index int `json:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindNearestPoint finds the nearest point from a list to a reference point
|
||||||
|
func FindNearestPoint(referenceLat, referenceLon float64, points [][]float64) *NearestPointResult {
|
||||||
|
if len(points) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var nearest *NearestPointResult
|
||||||
|
minDistance := math.MaxFloat64
|
||||||
|
|
||||||
|
for i, point := range points {
|
||||||
|
if len(point) < 2 {
|
||||||
|
continue // Skip invalid points
|
||||||
|
}
|
||||||
|
|
||||||
|
lat := point[0]
|
||||||
|
lon := point[1]
|
||||||
|
|
||||||
|
distance := Haversine(referenceLat, referenceLon, lat, lon)
|
||||||
|
|
||||||
|
if distance < minDistance {
|
||||||
|
minDistance = distance
|
||||||
|
nearest = &NearestPointResult{
|
||||||
|
Latitude: lat,
|
||||||
|
Longitude: lon,
|
||||||
|
DistanceKm: distance,
|
||||||
|
Index: i,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nearest
|
||||||
|
}
|
||||||
8
internal/domain/location_repository.go
Normal file
8
internal/domain/location_repository.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// LocationRepository defines the interface for loading locations
|
||||||
|
type LocationRepository interface {
|
||||||
|
LoadLocations(ctx context.Context, apiKey string) ([]Location, error)
|
||||||
|
}
|
||||||
11
internal/domain/person.go
Normal file
11
internal/domain/person.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
// Person represents a person from the list
|
||||||
|
type Person struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Surname string `json:"surname"`
|
||||||
|
Gender string `json:"gender"`
|
||||||
|
Born int `json:"born"`
|
||||||
|
City string `json:"city"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
}
|
||||||
13
internal/domain/person_report.go
Normal file
13
internal/domain/person_report.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
// PersonReport represents a complete report for one person
|
||||||
|
type PersonReport struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Surname string `json:"surname"`
|
||||||
|
NearestPlant string `json:"nearest_plant"`
|
||||||
|
PlantCode string `json:"plant_code"`
|
||||||
|
DistanceKm float64 `json:"distance_km"`
|
||||||
|
AccessLevel int `json:"access_level"`
|
||||||
|
PrimaryLatitude float64 `json:"primary_latitude"`
|
||||||
|
PrimaryLongitude float64 `json:"primary_longitude"`
|
||||||
|
}
|
||||||
32
internal/domain/report.go
Normal file
32
internal/domain/report.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "sort"
|
||||||
|
|
||||||
|
// PersonWithDistance represents a person with their distance to a location
|
||||||
|
type PersonWithDistance struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Surname string `json:"surname"`
|
||||||
|
LocationName string `json:"location_name"`
|
||||||
|
DistanceKm float64 `json:"distance_km"`
|
||||||
|
AccessLevel int `json:"access_level,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocationReport represents a report for a single location
|
||||||
|
type LocationReport struct {
|
||||||
|
LocationName string `json:"location_name"`
|
||||||
|
Persons []PersonWithDistance `json:"persons"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortPersonsByDistance sorts persons by distance (ascending)
|
||||||
|
func (r *LocationReport) SortPersonsByDistance() {
|
||||||
|
sort.Slice(r.Persons, func(i, j int) bool {
|
||||||
|
return r.Persons[i].DistanceKm < r.Persons[j].DistanceKm
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PersonData holds all gathered data for a person
|
||||||
|
type PersonData struct {
|
||||||
|
Person Person
|
||||||
|
Locations []PersonLocation // Multiple possible locations
|
||||||
|
AccessLevel int
|
||||||
|
}
|
||||||
8
internal/domain/repository.go
Normal file
8
internal/domain/repository.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// PersonRepository defines the interface for loading persons
|
||||||
|
type PersonRepository interface {
|
||||||
|
LoadPersons(ctx context.Context, filePath string) ([]Person, error)
|
||||||
|
}
|
||||||
132
internal/domain/tools.go
Normal file
132
internal/domain/tools.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
// GetToolDefinitions returns tool definitions for function calling
|
||||||
|
func GetToolDefinitions() []Tool {
|
||||||
|
return []Tool{
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: FunctionDef{
|
||||||
|
Name: "get_location",
|
||||||
|
Description: "Gets the current location information for a person. Returns latitude and longitude coordinates.",
|
||||||
|
Parameters: map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"name": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The first name of the person",
|
||||||
|
},
|
||||||
|
"surname": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The surname/last name of the person",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"name", "surname"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: FunctionDef{
|
||||||
|
Name: "get_access_level",
|
||||||
|
Description: "Gets the access level information for a person. Requires the person's birth year (only the year as integer, not full date). Returns access level data.",
|
||||||
|
Parameters: map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"name": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The first name of the person",
|
||||||
|
},
|
||||||
|
"surname": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The surname/last name of the person",
|
||||||
|
},
|
||||||
|
"birth_year": map[string]interface{}{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "The birth year of the person (only the year as integer, e.g., 1987, not full date)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"name", "surname", "birth_year"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: FunctionDef{
|
||||||
|
Name: "get_power_plant_location",
|
||||||
|
Description: "Gets the geographical coordinates (latitude and longitude) of a power plant by its city name.",
|
||||||
|
Parameters: map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"city_name": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the city where the power plant is located",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"city_name"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: FunctionDef{
|
||||||
|
Name: "calculate_distance",
|
||||||
|
Description: "Calculates the distance in kilometers between two geographical points using the Haversine formula.",
|
||||||
|
Parameters: map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"lat1": map[string]interface{}{
|
||||||
|
"type": "number",
|
||||||
|
"description": "Latitude of the first point",
|
||||||
|
},
|
||||||
|
"lon1": map[string]interface{}{
|
||||||
|
"type": "number",
|
||||||
|
"description": "Longitude of the first point",
|
||||||
|
},
|
||||||
|
"lat2": map[string]interface{}{
|
||||||
|
"type": "number",
|
||||||
|
"description": "Latitude of the second point",
|
||||||
|
},
|
||||||
|
"lon2": map[string]interface{}{
|
||||||
|
"type": "number",
|
||||||
|
"description": "Longitude of the second point",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"lat1", "lon1", "lat2", "lon2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: FunctionDef{
|
||||||
|
Name: "find_nearest_point",
|
||||||
|
Description: "Finds the nearest point from a list of points to a reference point (e.g., power plant location). Returns the nearest point with its distance.",
|
||||||
|
Parameters: map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"reference_lat": map[string]interface{}{
|
||||||
|
"type": "number",
|
||||||
|
"description": "Latitude of the reference point (e.g., power plant)",
|
||||||
|
},
|
||||||
|
"reference_lon": map[string]interface{}{
|
||||||
|
"type": "number",
|
||||||
|
"description": "Longitude of the reference point (e.g., power plant)",
|
||||||
|
},
|
||||||
|
"points": map[string]interface{}{
|
||||||
|
"type": "array",
|
||||||
|
"description": "Array of points to check, each point is [latitude, longitude]",
|
||||||
|
"items": map[string]interface{}{
|
||||||
|
"type": "array",
|
||||||
|
"items": map[string]interface{}{
|
||||||
|
"type": "number",
|
||||||
|
},
|
||||||
|
"minItems": 2,
|
||||||
|
"maxItems": 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"reference_lat", "reference_lon", "points"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
90
internal/infrastructure/api/client.go
Normal file
90
internal/infrastructure/api/client.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/paramah/ai_devs4/s01e02/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client implements domain.APIClient
|
||||||
|
type Client struct {
|
||||||
|
locationsURL string
|
||||||
|
accessLevelURL string
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new API client
|
||||||
|
func NewClient(locationsURL, accessLevelURL string) *Client {
|
||||||
|
return &Client{
|
||||||
|
locationsURL: locationsURL,
|
||||||
|
accessLevelURL: accessLevelURL,
|
||||||
|
httpClient: &http.Client{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocation sends a location request and returns the raw JSON response
|
||||||
|
func (c *Client) GetLocation(ctx context.Context, req domain.LocationRequest) ([]byte, error) {
|
||||||
|
jsonData, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshaling request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.locationsURL, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("sending request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAccessLevel sends an access level request and returns the raw JSON response
|
||||||
|
func (c *Client) GetAccessLevel(ctx context.Context, req domain.AccessLevelRequest) ([]byte, error) {
|
||||||
|
jsonData, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshaling request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.accessLevelURL, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("sending request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
59
internal/infrastructure/json/location_repository.go
Normal file
59
internal/infrastructure/json/location_repository.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package json
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/paramah/ai_devs4/s01e02/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LocationRepository implements domain.LocationRepository
|
||||||
|
type LocationRepository struct {
|
||||||
|
baseURL string
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLocationRepository creates a new location repository
|
||||||
|
func NewLocationRepository(baseURL string) *LocationRepository {
|
||||||
|
return &LocationRepository{
|
||||||
|
baseURL: baseURL,
|
||||||
|
client: &http.Client{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadLocations loads locations from the API
|
||||||
|
func (r *LocationRepository) LoadLocations(ctx context.Context, apiKey string) ([]domain.Location, error) {
|
||||||
|
url := fmt.Sprintf("%s/data/%s/findhim_locations.json", r.baseURL, apiKey)
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := r.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetching locations: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var plantsData domain.PowerPlantsData
|
||||||
|
if err := json.Unmarshal(body, &plantsData); err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
locations := plantsData.ToLocations()
|
||||||
|
return locations, nil
|
||||||
|
}
|
||||||
33
internal/infrastructure/json/repository.go
Normal file
33
internal/infrastructure/json/repository.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package json
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/paramah/ai_devs4/s01e02/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Repository implements domain.PersonRepository
|
||||||
|
type Repository struct{}
|
||||||
|
|
||||||
|
// NewRepository creates a new JSON repository
|
||||||
|
func NewRepository() *Repository {
|
||||||
|
return &Repository{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadPersons loads persons from a JSON file
|
||||||
|
func (r *Repository) LoadPersons(ctx context.Context, filePath string) ([]domain.Person, error) {
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var persons []domain.Person
|
||||||
|
if err := json.Unmarshal(data, &persons); err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return persons, nil
|
||||||
|
}
|
||||||
123
internal/infrastructure/llm/lmstudio.go
Normal file
123
internal/infrastructure/llm/lmstudio.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package llm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/paramah/ai_devs4/s01e02/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LMStudioProvider implements domain.LLMProvider for local LM Studio
|
||||||
|
type LMStudioProvider struct {
|
||||||
|
baseURL string
|
||||||
|
model string
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLMStudioProvider creates a new LM Studio provider
|
||||||
|
func NewLMStudioProvider(baseURL, model string) *LMStudioProvider {
|
||||||
|
return &LMStudioProvider{
|
||||||
|
baseURL: baseURL,
|
||||||
|
model: model,
|
||||||
|
client: &http.Client{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type lmStudioRequest struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Messages []domain.LLMMessage `json:"messages"`
|
||||||
|
Tools []domain.Tool `json:"tools,omitempty"`
|
||||||
|
ToolChoice interface{} `json:"tool_choice,omitempty"`
|
||||||
|
Temperature float64 `json:"temperature,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type lmStudioResponse struct {
|
||||||
|
Choices []struct {
|
||||||
|
Message domain.LLMMessage `json:"message"`
|
||||||
|
FinishReason string `json:"finish_reason"`
|
||||||
|
} `json:"choices"`
|
||||||
|
Error json.RawMessage `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chat sends a chat request with function calling support
|
||||||
|
func (p *LMStudioProvider) Chat(ctx context.Context, request domain.LLMRequest) (*domain.LLMResponse, error) {
|
||||||
|
reqBody := lmStudioRequest{
|
||||||
|
Model: p.model,
|
||||||
|
Messages: request.Messages,
|
||||||
|
Tools: request.Tools,
|
||||||
|
Temperature: request.Temperature,
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.ToolChoice != "" {
|
||||||
|
if request.ToolChoice == "auto" {
|
||||||
|
reqBody.ToolChoice = "auto"
|
||||||
|
} else {
|
||||||
|
reqBody.ToolChoice = map[string]interface{}{
|
||||||
|
"type": "function",
|
||||||
|
"function": map[string]string{
|
||||||
|
"name": request.ToolChoice,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshaling request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := p.baseURL + "/v1/chat/completions"
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := p.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("sending request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiResp lmStudioResponse
|
||||||
|
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshaling response: %w\nResponse body: %s", err, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(apiResp.Error) > 0 {
|
||||||
|
var errStr string
|
||||||
|
if err := json.Unmarshal(apiResp.Error, &errStr); err == nil {
|
||||||
|
return nil, fmt.Errorf("API error: %s", errStr)
|
||||||
|
}
|
||||||
|
var errObj struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(apiResp.Error, &errObj); err == nil {
|
||||||
|
return nil, fmt.Errorf("API error: %s", errObj.Message)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("API error: %s", string(apiResp.Error))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(apiResp.Choices) == 0 {
|
||||||
|
return nil, fmt.Errorf("no choices in response. Response body: %s", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &domain.LLMResponse{
|
||||||
|
Message: apiResp.Choices[0].Message,
|
||||||
|
FinishReason: apiResp.Choices[0].FinishReason,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
113
internal/infrastructure/llm/openrouter.go
Normal file
113
internal/infrastructure/llm/openrouter.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package llm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/paramah/ai_devs4/s01e02/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OpenRouterProvider implements domain.LLMProvider for OpenRouter API
|
||||||
|
type OpenRouterProvider struct {
|
||||||
|
apiKey string
|
||||||
|
model string
|
||||||
|
baseURL string
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOpenRouterProvider creates a new OpenRouter provider
|
||||||
|
func NewOpenRouterProvider(apiKey, model string) *OpenRouterProvider {
|
||||||
|
return &OpenRouterProvider{
|
||||||
|
apiKey: apiKey,
|
||||||
|
model: model,
|
||||||
|
baseURL: "https://openrouter.ai/api/v1/chat/completions",
|
||||||
|
client: &http.Client{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type openRouterRequest struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Messages []domain.LLMMessage `json:"messages"`
|
||||||
|
Tools []domain.Tool `json:"tools,omitempty"`
|
||||||
|
ToolChoice interface{} `json:"tool_choice,omitempty"`
|
||||||
|
Temperature float64 `json:"temperature,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type openRouterResponse struct {
|
||||||
|
Choices []struct {
|
||||||
|
Message domain.LLMMessage `json:"message"`
|
||||||
|
FinishReason string `json:"finish_reason"`
|
||||||
|
} `json:"choices"`
|
||||||
|
Error *struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
} `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chat sends a chat request with function calling support
|
||||||
|
func (p *OpenRouterProvider) Chat(ctx context.Context, request domain.LLMRequest) (*domain.LLMResponse, error) {
|
||||||
|
reqBody := openRouterRequest{
|
||||||
|
Model: p.model,
|
||||||
|
Messages: request.Messages,
|
||||||
|
Tools: request.Tools,
|
||||||
|
Temperature: request.Temperature,
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.ToolChoice != "" {
|
||||||
|
if request.ToolChoice == "auto" {
|
||||||
|
reqBody.ToolChoice = "auto"
|
||||||
|
} else {
|
||||||
|
reqBody.ToolChoice = map[string]interface{}{
|
||||||
|
"type": "function",
|
||||||
|
"function": map[string]string{
|
||||||
|
"name": request.ToolChoice,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshaling request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.baseURL, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+p.apiKey)
|
||||||
|
|
||||||
|
resp, err := p.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("sending request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiResp openRouterResponse
|
||||||
|
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshaling response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if apiResp.Error != nil {
|
||||||
|
return nil, fmt.Errorf("API error: %s", apiResp.Error.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(apiResp.Choices) == 0 {
|
||||||
|
return nil, fmt.Errorf("no choices in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &domain.LLMResponse{
|
||||||
|
Message: apiResp.Choices[0].Message,
|
||||||
|
FinishReason: apiResp.Choices[0].FinishReason,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
53
lista.json
Normal file
53
lista.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Cezary",
|
||||||
|
"surname": "Żurek",
|
||||||
|
"gender": "M",
|
||||||
|
"born": 1987,
|
||||||
|
"city": "Grudziądz",
|
||||||
|
"tags": [
|
||||||
|
"transport"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jacek",
|
||||||
|
"surname": "Nowak",
|
||||||
|
"gender": "M",
|
||||||
|
"born": 1991,
|
||||||
|
"city": "Grudziądz",
|
||||||
|
"tags": [
|
||||||
|
"transport"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Oskar",
|
||||||
|
"surname": "Sieradzki",
|
||||||
|
"gender": "M",
|
||||||
|
"born": 1993,
|
||||||
|
"city": "Grudziądz",
|
||||||
|
"tags": [
|
||||||
|
"transport"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Wojciech",
|
||||||
|
"surname": "Bielik",
|
||||||
|
"gender": "M",
|
||||||
|
"born": 1986,
|
||||||
|
"city": "Grudziądz",
|
||||||
|
"tags": [
|
||||||
|
"transport"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Wacław",
|
||||||
|
"surname": "Jasiński",
|
||||||
|
"gender": "M",
|
||||||
|
"born": 1986,
|
||||||
|
"city": "Grudziądz",
|
||||||
|
"tags": [
|
||||||
|
"transport",
|
||||||
|
"praca z pojazdami"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user