prise en charge des outils + weather
This commit is contained in:
1 parent
a36f1231cd
commit
0a6ffab050
14 files changed
+623
-113
No files matched your search
@@ -67,18 +67,43 @@ func (r *TursoChatRepository) DeleteChat(userID string, teamID string, id string
|
||||
)
|
||||
}
|
||||
|
||||
func (r *TursoChatRepository) GetChatMessages(userID string, teamID string, chatID string) ([]*domain.Message, error) {
|
||||
return r.messageTable.Select(
|
||||
orm.WithWhere(
|
||||
"chat_id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
|
||||
chatID,
|
||||
teamID,
|
||||
userID,
|
||||
),
|
||||
orm.WithOrder("_date_created asc"),
|
||||
)
|
||||
func (r *TursoChatRepository) GetChatMessages(userID string, teamID string, chatID string, withToolCallResponses bool) ([]*domain.Message, error) {
|
||||
var messages []*domain.Message
|
||||
var err error
|
||||
if !withToolCallResponses {
|
||||
messages, err = r.messageTable.Select(
|
||||
orm.WithWhere(
|
||||
`chat_id=$1
|
||||
and team_id=$2
|
||||
and team_id in (select team_id from user_teams where user_id=$3)
|
||||
and (tool_call_id='' or tool_call_id is null)`,
|
||||
chatID,
|
||||
teamID,
|
||||
userID,
|
||||
),
|
||||
orm.WithOrder("_date_created asc"),
|
||||
)
|
||||
} else {
|
||||
messages, err = r.messageTable.Select(
|
||||
orm.WithWhere(
|
||||
"chat_id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
|
||||
chatID,
|
||||
teamID,
|
||||
userID,
|
||||
),
|
||||
orm.WithOrder("_date_created asc"),
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, m := range messages {
|
||||
m.Json2toolCall()
|
||||
}
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func (r *TursoChatRepository) CreateChatMessage(userID string, message *domain.Message) (*domain.Message, error) {
|
||||
message.ToolCall2Json()
|
||||
return r.messageTable.Insert(message)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package llm
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
anyllm "github.com/mozilla-ai/any-llm-go"
|
||||
"github.com/mozilla-ai/any-llm-go/providers"
|
||||
@@ -82,11 +83,14 @@ func (e *AnyLLMEngine) Stream(
|
||||
modelID string,
|
||||
params *domain.LLMParams,
|
||||
messages []*domain.Message,
|
||||
) (*domain.Message, error) {
|
||||
) *domain.Message {
|
||||
|
||||
p, err := providerFactory(provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return &domain.Message{
|
||||
Role: string(domain.RoleAssistant),
|
||||
Content: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
anyllmMessages := make([]anyllm.Message, 0)
|
||||
@@ -101,18 +105,27 @@ func (e *AnyLLMEngine) Stream(
|
||||
params.ReasoningEffort = "none"
|
||||
}
|
||||
|
||||
tools := make([]providers.Tool, len(params.Tools))
|
||||
for i, t := range params.Tools {
|
||||
tools[i] = toolConvert(t)
|
||||
}
|
||||
|
||||
toolCalling := false
|
||||
toolCalls := make([]domain.ToolCall, 0)
|
||||
|
||||
chunkChan, errChan := p.CompletionStream(ctx, anyllm.CompletionParams{
|
||||
Model: modelID,
|
||||
Messages: anyllmMessages,
|
||||
Stream: true,
|
||||
ReasoningEffort: providers.ReasoningEffort(params.ReasoningEffort),
|
||||
Tools: tools,
|
||||
})
|
||||
|
||||
fullContent := ""
|
||||
role := anyllm.RoleAssistant
|
||||
|
||||
for chunk := range chunkChan {
|
||||
if len(chunk.Choices) > 0 {
|
||||
|
||||
content := chunk.Choices[0].Delta.Content
|
||||
reasoning := chunk.Choices[0].Delta.Reasoning
|
||||
if params.OnChunk != nil {
|
||||
@@ -120,34 +133,82 @@ func (e *AnyLLMEngine) Stream(
|
||||
fullContent += content
|
||||
params.OnChunk(&domain.Chunk{
|
||||
Done: false,
|
||||
Role: role,
|
||||
Role: anyllm.RoleAssistant,
|
||||
Content: content,
|
||||
Reasoning: false,
|
||||
})
|
||||
} else if reasoning != nil {
|
||||
params.OnChunk(&domain.Chunk{
|
||||
Done: false,
|
||||
Role: role,
|
||||
Role: anyllm.RoleAssistant,
|
||||
Content: reasoning.Content,
|
||||
Reasoning: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range chunk.Choices[0].Delta.ToolCalls {
|
||||
toolCalling = true
|
||||
slog.Info("Model is calling tool", "tool", tc.Function.Name, "arguments", tc.Function.Arguments)
|
||||
|
||||
extra := make(map[string]map[string]any)
|
||||
for k, v := range tc.Extra {
|
||||
m := map[string]any(v)
|
||||
extra[k] = m
|
||||
}
|
||||
|
||||
toolCalls = append(toolCalls, domain.ToolCall{
|
||||
ID: tc.ID,
|
||||
Type: tc.Type,
|
||||
Function: domain.ToolCallFunction{
|
||||
Name: tc.Function.Name,
|
||||
Arguments: tc.Function.Arguments,
|
||||
},
|
||||
Extra: extra,
|
||||
})
|
||||
|
||||
params.OnChunk(&domain.Chunk{
|
||||
Done: false,
|
||||
ToolName: tc.Function.Name,
|
||||
Role: anyllm.RoleAssistant,
|
||||
Content: tc.Function.Arguments,
|
||||
Reasoning: false,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
params.OnChunk(&domain.Chunk{
|
||||
Done: true,
|
||||
Role: role,
|
||||
Content: "",
|
||||
})
|
||||
if !toolCalling {
|
||||
params.OnChunk(&domain.Chunk{
|
||||
Done: true,
|
||||
Role: anyllm.RoleAssistant,
|
||||
Content: "",
|
||||
})
|
||||
}
|
||||
|
||||
if err := <-errChan; err != nil {
|
||||
return nil, err
|
||||
return &domain.Message{
|
||||
Role: string(domain.RoleAssistant),
|
||||
Content: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
return &domain.Message{
|
||||
Role: role,
|
||||
Content: fullContent,
|
||||
}, nil
|
||||
Role: string(domain.RoleAssistant),
|
||||
Content: fullContent,
|
||||
ToolCalls: toolCalls,
|
||||
}
|
||||
}
|
||||
|
||||
func toolConvert(domainTool *domain.ToolDefinition) providers.Tool {
|
||||
return providers.Tool{
|
||||
Type: domainTool.Type,
|
||||
Function: providers.Function{
|
||||
Name: domainTool.Function.Name,
|
||||
Description: domainTool.Function.Description,
|
||||
Parameters: domainTool.Function.Parameters,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package weather
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"trankilou.fr/lassistanoque/backend/internal/domain"
|
||||
)
|
||||
|
||||
type WeatherTool struct {
|
||||
}
|
||||
|
||||
type WeatherConfig struct {
|
||||
}
|
||||
|
||||
func (t *WeatherTool) Name() string {
|
||||
return "weather"
|
||||
}
|
||||
|
||||
func (t *WeatherTool) Description() string {
|
||||
return "Get the current weather for a given location"
|
||||
}
|
||||
|
||||
func (t *WeatherTool) Definition(ctx context.Context) *domain.ToolDefinition {
|
||||
return &domain.ToolDefinition{
|
||||
Type: "function",
|
||||
Function: &domain.ToolFunction{
|
||||
Name: t.Name(),
|
||||
Description: t.Description(),
|
||||
Parameters: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"location": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The city name, e.g., 'Paris' or 'New York'",
|
||||
},
|
||||
},
|
||||
"required": []string{"location"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *WeatherTool) Execute(ctx context.Context, input []byte) ([]byte, error) {
|
||||
|
||||
var params struct {
|
||||
Location string `json:"location"`
|
||||
}
|
||||
err := json.Unmarshal(input, ¶ms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := http.Get("https://geocoding-api.open-meteo.com/v1/search?name=" + url.QueryEscape(params.Location))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var location struct {
|
||||
Results []struct {
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
} `json:"results"`
|
||||
}
|
||||
err = json.NewDecoder(resp.Body).Decode(&location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(location.Results) == 0 {
|
||||
return nil, fmt.Errorf("location not found : %s", input)
|
||||
}
|
||||
|
||||
lon := location.Results[0].Longitude
|
||||
lat := location.Results[0].Latitude
|
||||
|
||||
weatherResp, err := http.Get(fmt.Sprintf("https://api.open-meteo.com/v1/forecast?latitude=%f&longitude=%f¤t_weather=true&daily=weather_code,temperature_2m_max,temperature_2m_min,rain_sum,wind_speed_10m_max&hourly=temperature_2m&forecast_days=15", lat, lon))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer weatherResp.Body.Close()
|
||||
|
||||
var weatherData struct {
|
||||
CurrentWeatherUnits struct {
|
||||
Temperature string `json:"temperature"`
|
||||
WindSpeed string `json:"windspeed"`
|
||||
} `json:"current_weather_units"`
|
||||
CurrentWeather struct {
|
||||
Time string `json:"time"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
WeatherCode int `json:"weathercode"`
|
||||
} `json:"current_weather"`
|
||||
DailyUnits struct {
|
||||
TemperatureMax string `json:"temperature_2m_max"`
|
||||
TemperatureMin string `json:"temperature_2m_min"`
|
||||
Rain string `json:"rain_sum"`
|
||||
WindSpeed string `json:"wind_speed_10m_max"`
|
||||
} `json:"daily_units"`
|
||||
Daily struct {
|
||||
Time []string `json:"time"`
|
||||
TemperatureMax []float64 `json:"temperature_2m_max"`
|
||||
TemperatureMin []float64 `json:"temperature_2m_min"`
|
||||
Rain []float64 `json:"rain_sum"`
|
||||
WindSpeed []float64 `json:"wind_speed_10m_max"`
|
||||
} `json:"daily"`
|
||||
}
|
||||
|
||||
err = json.NewDecoder(weatherResp.Body).Decode(&weatherData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
|
||||
fmt.Fprintf(&builder, "Current weather in %s\n", params.Location)
|
||||
fmt.Fprintf(&builder, "- Time: %s\n", weatherData.CurrentWeather.Time)
|
||||
fmt.Fprintf(&builder, "- Temperature: %.2f %s\n", weatherData.CurrentWeather.Temperature, weatherData.CurrentWeatherUnits.Temperature)
|
||||
fmt.Fprintf(&builder, "- Weather code: %d - %s\n", weatherData.CurrentWeather.WeatherCode, wmoCodeToDescription(weatherData.CurrentWeather.WeatherCode))
|
||||
if len(weatherData.Daily.WindSpeed) > 0 {
|
||||
fmt.Fprintf(&builder, "- Wind speed: %.2f %s\n", weatherData.Daily.WindSpeed[0], weatherData.DailyUnits.WindSpeed)
|
||||
}
|
||||
if len(weatherData.Daily.TemperatureMax) > 0 {
|
||||
fmt.Fprintf(&builder, "- Max temperature: %.2f %s\n", weatherData.Daily.TemperatureMax[0], weatherData.DailyUnits.TemperatureMax)
|
||||
}
|
||||
if len(weatherData.Daily.TemperatureMin) > 0 {
|
||||
fmt.Fprintf(&builder, "- Min temperature: %.2f %s\n", weatherData.Daily.TemperatureMin[0], weatherData.DailyUnits.TemperatureMin)
|
||||
}
|
||||
if len(weatherData.Daily.Rain) > 0 {
|
||||
fmt.Fprintf(&builder, "- Rainfall: %.2f %s\n", weatherData.Daily.Rain[0], weatherData.DailyUnits.Rain)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&builder, "\nForecast for the next days:\n")
|
||||
|
||||
// Iterate over the daily data and append to the repor
|
||||
for i := 0; i < len(weatherData.Daily.Time); i++ {
|
||||
fmt.Fprintf(&builder, "%s: Max Temp: %.2f %s, Min Temp: %.2f %s, Rain: %.2f %s, Wind Speed: %.2f %s\n",
|
||||
weatherData.Daily.Time[i],
|
||||
weatherData.Daily.TemperatureMax[i], weatherData.DailyUnits.TemperatureMax,
|
||||
weatherData.Daily.TemperatureMin[i], weatherData.DailyUnits.TemperatureMin,
|
||||
weatherData.Daily.Rain[i], weatherData.DailyUnits.Rain,
|
||||
weatherData.Daily.WindSpeed[i], weatherData.DailyUnits.WindSpeed)
|
||||
}
|
||||
|
||||
return []byte(builder.String()), nil
|
||||
}
|
||||
|
||||
func wmoCodeToDescription(code int) string {
|
||||
switch code {
|
||||
case 0:
|
||||
return "Clear sky"
|
||||
case 1, 2, 3:
|
||||
return "Mainly clear, partly cloudy, and overcast"
|
||||
case 45, 48:
|
||||
return "Fog and depositing rime fog"
|
||||
case 51, 53, 55:
|
||||
return "Drizzle: Light, moderate, and dense intensity"
|
||||
case 56, 57:
|
||||
return "Freezing Drizzle: Light and dense intensity"
|
||||
case 61, 63, 65:
|
||||
return "Rain: Slight, moderate and heavy intensity"
|
||||
case 66, 67:
|
||||
return "Freezing Rain: Light and heavy intensity"
|
||||
case 71, 73, 75:
|
||||
return "Snowfall: Slight, moderate, and heavy intensity"
|
||||
case 77:
|
||||
return "Snow grains"
|
||||
case 80, 81, 82:
|
||||
return "Rain showers: Slight, moderate, and violent"
|
||||
case 85, 86:
|
||||
return "Snow showers slight and heavy"
|
||||
case 95:
|
||||
return "Thunderstorm: Slight and heavy intensity"
|
||||
case 96, 99:
|
||||
return "Thunderstorm with slight and heavy hail"
|
||||
default:
|
||||
return "N/A"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user