blob: 1b2783194bca5844a275abc492ec655850c728a0 [file] [log] [blame]
iomodob67a3762025-07-25 20:27:04 +04001package agent
2
iomodo76f9a2d2025-07-26 12:14:40 +04003import (
4 "context"
5 "fmt"
iomodo0c203b12025-07-26 19:44:57 +04006 "log/slog"
iomodo76f9a2d2025-07-26 12:14:40 +04007 "os"
8 "path/filepath"
9 "strings"
10 "time"
11
12 "github.com/iomodo/staff/git"
13 "github.com/iomodo/staff/llm"
iomodo0c203b12025-07-26 19:44:57 +040014 _ "github.com/iomodo/staff/llm/openai" // Import for side effects (registers provider)
iomodo76f9a2d2025-07-26 12:14:40 +040015 "github.com/iomodo/staff/tm"
16)
17
18// AgentConfig contains configuration for the agent
iomodob67a3762025-07-25 20:27:04 +040019type AgentConfig struct {
20 Name string
21 Role string
22 GitUsername string
23 GitEmail string
24 WorkingDir string
iomodo76f9a2d2025-07-26 12:14:40 +040025
26 // LLM Configuration
27 LLMProvider llm.Provider
28 LLMModel string
29 LLMConfig llm.Config
30
31 // System prompt for the agent
32 SystemPrompt string
33
34 // Task Manager Configuration
35 TaskManager tm.TaskManager
36
37 // Git Configuration
38 GitRepoPath string
39 GitRemote string
40 GitBranch string
iomodo570d4262025-07-26 16:26:36 +040041
42 // Gerrit Configuration
43 GerritEnabled bool
44 GerritConfig GerritConfig
45}
46
47// GerritConfig holds configuration for Gerrit operations
48type GerritConfig struct {
49 Username string
50 Password string // Can be HTTP password or API token
51 BaseURL string
52 Project string
iomodob67a3762025-07-25 20:27:04 +040053}
54
iomodo76f9a2d2025-07-26 12:14:40 +040055// Agent represents an AI agent that can process tasks
iomodob67a3762025-07-25 20:27:04 +040056type Agent struct {
iomodo76f9a2d2025-07-26 12:14:40 +040057 Config AgentConfig
58 llmProvider llm.LLMProvider
59 gitInterface git.GitInterface
60 ctx context.Context
61 cancel context.CancelFunc
iomodo0c203b12025-07-26 19:44:57 +040062 logger *slog.Logger
iomodob67a3762025-07-25 20:27:04 +040063}
64
iomodo76f9a2d2025-07-26 12:14:40 +040065// NewAgent creates a new agent instance
iomodo0c203b12025-07-26 19:44:57 +040066func NewAgent(config AgentConfig, logger *slog.Logger) (*Agent, error) {
iomodo76f9a2d2025-07-26 12:14:40 +040067 // Validate configuration
68 if err := validateConfig(config); err != nil {
69 return nil, fmt.Errorf("invalid config: %w", err)
70 }
71
72 // Create LLM provider
73 llmProvider, err := llm.CreateProvider(config.LLMConfig)
74 if err != nil {
75 return nil, fmt.Errorf("failed to create LLM provider: %w", err)
76 }
77
78 // Create git interface
iomodo570d4262025-07-26 16:26:36 +040079 var gitInterface git.GitInterface
80 if config.GerritEnabled {
81 // Create Gerrit pull request provider
82 gerritPRProvider := git.NewGerritPullRequestProvider(config.GerritConfig.Project, git.GerritConfig{
83 Username: config.GerritConfig.Username,
84 Password: config.GerritConfig.Password,
85 BaseURL: config.GerritConfig.BaseURL,
86 HTTPClient: nil, // Will use default client
87 })
88
89 // Create git interface with Gerrit pull request provider
90 gitConfig := git.GitConfig{
91 Timeout: 30 * time.Second,
92 PullRequestProvider: gerritPRProvider,
93 }
iomodo0c203b12025-07-26 19:44:57 +040094 gitInterface = git.NewGitWithPullRequests(config.GitRepoPath, gitConfig, gerritPRProvider, logger)
iomodo570d4262025-07-26 16:26:36 +040095 } else {
96 // Use default git interface (GitHub)
97 gitInterface = git.DefaultGit(config.GitRepoPath)
98 }
iomodo76f9a2d2025-07-26 12:14:40 +040099
100 // Create context with cancellation
101 ctx, cancel := context.WithCancel(context.Background())
102
103 agent := &Agent{
104 Config: config,
105 llmProvider: llmProvider,
106 gitInterface: gitInterface,
107 ctx: ctx,
108 cancel: cancel,
iomodo0c203b12025-07-26 19:44:57 +0400109 logger: logger,
iomodo76f9a2d2025-07-26 12:14:40 +0400110 }
111
112 return agent, nil
113}
114
115// validateConfig validates the agent configuration
116func validateConfig(config AgentConfig) error {
117 if config.Name == "" {
118 return fmt.Errorf("agent name is required")
119 }
120 if config.Role == "" {
121 return fmt.Errorf("agent role is required")
122 }
123 if config.WorkingDir == "" {
124 return fmt.Errorf("working directory is required")
125 }
126 if config.SystemPrompt == "" {
127 return fmt.Errorf("system prompt is required")
128 }
129 if config.TaskManager == nil {
130 return fmt.Errorf("task manager is required")
131 }
132 if config.GitRepoPath == "" {
133 return fmt.Errorf("git repository path is required")
134 }
135 return nil
136}
137
iomodo6cf9dda2025-07-27 15:18:07 +0400138const (
139 // Agent polling intervals
140 TaskPollingInterval = 60 * time.Second
141 ErrorRetryInterval = 30 * time.Second
142 DefaultGitTimeout = 30 * time.Second
143 DefaultContextTimeout = 5 * time.Minute
144 DefaultMaxTaskRetries = 3
145 DefaultLLMMaxTokens = 4000
146 DefaultLLMTemperature = 0.7
147)
148
iomodo76f9a2d2025-07-26 12:14:40 +0400149// Run starts the agent's main loop
150func (a *Agent) Run() error {
iomodo0c203b12025-07-26 19:44:57 +0400151 a.logger.Info("Starting agent", slog.String("name", a.Config.Name), slog.String("role", a.Config.Role))
152 defer a.logger.Info("Agent stopped", slog.String("name", a.Config.Name))
iomodo76f9a2d2025-07-26 12:14:40 +0400153
154 // Initialize git repository if needed
155 if err := a.initializeGit(); err != nil {
156 return fmt.Errorf("failed to initialize git: %w", err)
157 }
158
159 // Main agent loop
160 for {
161 select {
162 case <-a.ctx.Done():
163 return a.ctx.Err()
164 default:
165 if err := a.processNextTask(); err != nil {
iomodo0c203b12025-07-26 19:44:57 +0400166 a.logger.Error("Error processing task", slog.String("error", err.Error()))
iomodo6cf9dda2025-07-27 15:18:07 +0400167 time.Sleep(ErrorRetryInterval)
iomodo76f9a2d2025-07-26 12:14:40 +0400168 }
169 }
iomodob67a3762025-07-25 20:27:04 +0400170 }
171}
172
iomodo76f9a2d2025-07-26 12:14:40 +0400173// Stop stops the agent
174func (a *Agent) Stop() {
iomodo0c203b12025-07-26 19:44:57 +0400175 a.logger.Info("Stopping agent", slog.String("name", a.Config.Name))
iomodo76f9a2d2025-07-26 12:14:40 +0400176 a.cancel()
177 if a.llmProvider != nil {
178 a.llmProvider.Close()
179 }
180}
iomodob67a3762025-07-25 20:27:04 +0400181
iomodo76f9a2d2025-07-26 12:14:40 +0400182// initializeGit initializes the git repository
183func (a *Agent) initializeGit() error {
184 ctx := context.Background()
185
iomodo6cf9dda2025-07-27 15:18:07 +0400186 if err := a.ensureRepository(ctx); err != nil {
187 return err
188 }
189
190 if err := a.ensureRemoteOrigin(ctx); err != nil {
191 return err
192 }
193
194 if err := a.ensureTargetBranch(ctx); err != nil {
195 return err
196 }
197
198 return nil
199}
200
201// ensureRepository ensures the git repository is initialized
202func (a *Agent) ensureRepository(ctx context.Context) error {
iomodo76f9a2d2025-07-26 12:14:40 +0400203 isRepo, err := a.gitInterface.IsRepository(ctx, a.Config.GitRepoPath)
204 if err != nil {
205 return fmt.Errorf("failed to check repository: %w", err)
206 }
207
208 if !isRepo {
iomodo76f9a2d2025-07-26 12:14:40 +0400209 if err := a.gitInterface.Init(ctx, a.Config.GitRepoPath); err != nil {
210 return fmt.Errorf("failed to initialize repository: %w", err)
211 }
212 }
213
iomodo6cf9dda2025-07-27 15:18:07 +0400214 return nil
215}
216
217// ensureRemoteOrigin ensures the remote origin is configured
218func (a *Agent) ensureRemoteOrigin(ctx context.Context) error {
iomodo570d4262025-07-26 16:26:36 +0400219 remotes, err := a.gitInterface.ListRemotes(ctx)
220 if err != nil {
221 return fmt.Errorf("failed to list remotes: %w", err)
222 }
223
iomodo6cf9dda2025-07-27 15:18:07 +0400224 // Check if origin already exists
iomodo570d4262025-07-26 16:26:36 +0400225 for _, remote := range remotes {
226 if remote.Name == "origin" {
iomodo6cf9dda2025-07-27 15:18:07 +0400227 return nil
iomodo570d4262025-07-26 16:26:36 +0400228 }
229 }
230
iomodo6cf9dda2025-07-27 15:18:07 +0400231 // Add remote origin
232 remoteURL := a.buildRemoteURL()
233 if err := a.gitInterface.AddRemote(ctx, "origin", remoteURL); err != nil {
234 return fmt.Errorf("failed to add remote origin: %w", err)
iomodo76f9a2d2025-07-26 12:14:40 +0400235 }
236
237 return nil
238}
239
iomodo6cf9dda2025-07-27 15:18:07 +0400240// buildRemoteURL builds the appropriate remote URL based on configuration
241func (a *Agent) buildRemoteURL() string {
242 if !a.Config.GerritEnabled {
243 return a.Config.GitRemote
244 }
245
246 // Build Gerrit URL
247 if strings.HasPrefix(a.Config.GerritConfig.BaseURL, "https://") {
248 return fmt.Sprintf("%s/%s.git", a.Config.GerritConfig.BaseURL, a.Config.GerritConfig.Project)
249 }
250
251 // SSH format
252 return fmt.Sprintf("ssh://%s@%s:29418/%s.git",
253 a.Config.GerritConfig.Username,
254 strings.TrimPrefix(a.Config.GerritConfig.BaseURL, "https://"),
255 a.Config.GerritConfig.Project)
256}
257
258// ensureTargetBranch ensures the agent is on the target branch
259func (a *Agent) ensureTargetBranch(ctx context.Context) error {
260 if a.Config.GitBranch == "" {
261 return nil
262 }
263
264 currentBranch, err := a.gitInterface.GetCurrentBranch(ctx)
265 if err != nil {
266 return fmt.Errorf("failed to get current branch: %w", err)
267 }
268
269 if currentBranch == a.Config.GitBranch {
270 a.logger.Info("Already on target branch", slog.String("branch", a.Config.GitBranch))
271 return nil
272 }
273
274 return a.checkoutOrCreateBranch(ctx, a.Config.GitBranch)
275}
276
277// checkoutOrCreateBranch attempts to checkout a branch, creating it if it doesn't exist
278func (a *Agent) checkoutOrCreateBranch(ctx context.Context, branchName string) error {
279 if err := a.gitInterface.Checkout(ctx, branchName); err != nil {
280 if a.isBranchNotFoundError(err) {
281 if createErr := a.gitInterface.CreateBranch(ctx, branchName, ""); createErr != nil {
282 return fmt.Errorf("failed to create branch %s: %w", branchName, createErr)
283 }
284 return nil
285 }
286 return fmt.Errorf("failed to checkout branch %s: %w", branchName, err)
287 }
288 return nil
289}
290
291// isBranchNotFoundError checks if the error indicates a branch doesn't exist
292func (a *Agent) isBranchNotFoundError(err error) bool {
293 errMsg := err.Error()
294 return strings.Contains(errMsg, "did not match any file(s) known to git") ||
295 strings.Contains(errMsg, "not found") ||
296 strings.Contains(errMsg, "unknown revision") ||
297 strings.Contains(errMsg, "reference is not a tree") ||
298 strings.Contains(errMsg, "pathspec") ||
299 strings.Contains(errMsg, "fatal: invalid reference")
300}
301
iomodo76f9a2d2025-07-26 12:14:40 +0400302// processNextTask processes the next available task
303func (a *Agent) processNextTask() error {
304 ctx := context.Background()
305
306 // Get tasks assigned to this agent
307 taskList, err := a.Config.TaskManager.GetTasksByOwner(ctx, a.Config.Name, 0, 10)
iomodo97555d02025-07-27 15:07:14 +0400308 a.logger.Info("Total number of Tasks", slog.String("agent", a.Config.Name), slog.Any("tasks", taskList.TotalCount))
iomodo76f9a2d2025-07-26 12:14:40 +0400309 if err != nil {
310 return fmt.Errorf("failed to get tasks: %w", err)
311 }
312
313 // Find a task that's ready to be worked on
314 var taskToProcess *tm.Task
315 for _, task := range taskList.Tasks {
316 if task.Status == tm.StatusToDo {
317 taskToProcess = task
318 break
319 }
320 }
321
iomodo97555d02025-07-27 15:07:14 +0400322 a.logger.Info("Task to process", slog.Any("task", taskToProcess))
323
iomodo76f9a2d2025-07-26 12:14:40 +0400324 if taskToProcess == nil {
325 // No tasks to process, wait a bit
iomodo6cf9dda2025-07-27 15:18:07 +0400326 time.Sleep(TaskPollingInterval)
iomodo76f9a2d2025-07-26 12:14:40 +0400327 return nil
328 }
329
iomodo0c203b12025-07-26 19:44:57 +0400330 a.logger.Info("Processing task", slog.String("id", taskToProcess.ID), slog.String("title", taskToProcess.Title))
iomodo76f9a2d2025-07-26 12:14:40 +0400331
332 // Start the task
333 startedTask, err := a.Config.TaskManager.StartTask(ctx, taskToProcess.ID)
334 if err != nil {
335 return fmt.Errorf("failed to start task: %w", err)
336 }
337
338 // Process the task with LLM
339 solution, err := a.processTaskWithLLM(startedTask)
340 if err != nil {
341 // Mark task as failed or retry
iomodo0c203b12025-07-26 19:44:57 +0400342 a.logger.Error("Failed to process task with LLM", slog.String("error", err.Error()))
iomodo76f9a2d2025-07-26 12:14:40 +0400343 return err
344 }
345
346 // Create PR with the solution
347 if err := a.createPullRequest(startedTask, solution); err != nil {
348 return fmt.Errorf("failed to create pull request: %w", err)
349 }
350
351 // Complete the task
352 if _, err := a.Config.TaskManager.CompleteTask(ctx, startedTask.ID); err != nil {
353 return fmt.Errorf("failed to complete task: %w", err)
354 }
355
iomodo0c203b12025-07-26 19:44:57 +0400356 a.logger.Info("Successfully completed task", slog.String("id", startedTask.ID))
iomodo76f9a2d2025-07-26 12:14:40 +0400357 return nil
358}
359
360// processTaskWithLLM sends the task to the LLM and gets a solution
361func (a *Agent) processTaskWithLLM(task *tm.Task) (string, error) {
362 ctx := context.Background()
363
364 // Prepare the prompt
365 prompt := a.buildTaskPrompt(task)
366
367 // Create chat completion request
368 req := llm.ChatCompletionRequest{
369 Model: a.Config.LLMModel,
370 Messages: []llm.Message{
371 {
372 Role: llm.RoleSystem,
373 Content: a.Config.SystemPrompt,
374 },
375 {
376 Role: llm.RoleUser,
377 Content: prompt,
378 },
379 },
iomodo6cf9dda2025-07-27 15:18:07 +0400380 MaxTokens: intPtr(DefaultLLMMaxTokens),
381 Temperature: float64Ptr(DefaultLLMTemperature),
iomodo76f9a2d2025-07-26 12:14:40 +0400382 }
383
384 // Get response from LLM
385 resp, err := a.llmProvider.ChatCompletion(ctx, req)
386 if err != nil {
387 return "", fmt.Errorf("LLM chat completion failed: %w", err)
388 }
389
390 if len(resp.Choices) == 0 {
391 return "", fmt.Errorf("no response from LLM")
392 }
393
394 return resp.Choices[0].Message.Content, nil
395}
396
397// buildTaskPrompt builds the prompt for the LLM based on the task
398func (a *Agent) buildTaskPrompt(task *tm.Task) string {
399 var prompt strings.Builder
400
401 prompt.WriteString(fmt.Sprintf("Task ID: %s\n", task.ID))
402 prompt.WriteString(fmt.Sprintf("Title: %s\n", task.Title))
403 prompt.WriteString(fmt.Sprintf("Priority: %s\n", task.Priority))
404
405 if task.Description != "" {
406 prompt.WriteString(fmt.Sprintf("Description: %s\n", task.Description))
407 }
408
409 if task.DueDate != nil {
410 prompt.WriteString(fmt.Sprintf("Due Date: %s\n", task.DueDate.Format("2006-01-02")))
411 }
412
413 prompt.WriteString("\nPlease provide a detailed solution for this task. ")
414 prompt.WriteString("Include any code, documentation, or other deliverables as needed. ")
415 prompt.WriteString("Format your response appropriately for the type of task.")
416
417 return prompt.String()
418}
419
420// createPullRequest creates a pull request with the solution
421func (a *Agent) createPullRequest(task *tm.Task, solution string) error {
422 ctx := context.Background()
423
424 // Generate branch name
425 branchName := a.generateBranchName(task)
426
427 // Create and checkout to new branch
428 if err := a.gitInterface.CreateBranch(ctx, branchName, ""); err != nil {
429 return fmt.Errorf("failed to create branch: %w", err)
430 }
431
432 if err := a.gitInterface.Checkout(ctx, branchName); err != nil {
433 return fmt.Errorf("failed to checkout branch: %w", err)
434 }
435
436 // Create solution file
437 solutionPath := filepath.Join(a.Config.WorkingDir, fmt.Sprintf("task-%s-solution.md", task.ID))
438 solutionContent := a.formatSolution(task, solution)
439
440 if err := os.WriteFile(solutionPath, []byte(solutionContent), 0644); err != nil {
441 return fmt.Errorf("failed to write solution file: %w", err)
442 }
443
444 // Add and commit the solution
445 if err := a.gitInterface.Add(ctx, []string{solutionPath}); err != nil {
446 return fmt.Errorf("failed to add solution file: %w", err)
447 }
448
iomodo570d4262025-07-26 16:26:36 +0400449 commitMessage := fmt.Sprintf("feat: Complete task %s - %s\n\n%s", task.ID, task.Title, a.formatPullRequestDescription(task, solution))
iomodo7d08e8e2025-07-26 15:24:42 +0400450 if err := a.gitInterface.Commit(ctx, commitMessage, git.CommitOptions{
451 Author: &git.Author{
452 Name: a.Config.GitUsername,
453 Email: a.Config.GitEmail,
454 Time: time.Now(),
455 },
456 }); err != nil {
iomodo76f9a2d2025-07-26 12:14:40 +0400457 return fmt.Errorf("failed to commit solution: %w", err)
458 }
459
iomodo570d4262025-07-26 16:26:36 +0400460 if a.Config.GerritEnabled {
461 // For Gerrit: Push to refs/for/BRANCH to create a change
462 gerritRef := fmt.Sprintf("refs/for/%s", a.Config.GitBranch)
463 if err := a.gitInterface.Push(ctx, "origin", gerritRef, git.PushOptions{}); err != nil {
464 return fmt.Errorf("failed to push to Gerrit: %w", err)
465 }
iomodo0c203b12025-07-26 19:44:57 +0400466 a.logger.Info("Created Gerrit change for task", slog.String("id", task.ID), slog.String("ref", gerritRef))
iomodo570d4262025-07-26 16:26:36 +0400467 } else {
468 // For GitHub: Push branch and create PR
469 if err := a.gitInterface.Push(ctx, "origin", branchName, git.PushOptions{SetUpstream: true}); err != nil {
470 return fmt.Errorf("failed to push branch: %w", err)
471 }
472
473 // Create pull request using the git interface
474 prOptions := git.PullRequestOptions{
475 Title: fmt.Sprintf("Complete task %s: %s", task.ID, task.Title),
476 Description: a.formatPullRequestDescription(task, solution),
477 BaseBranch: a.Config.GitBranch,
478 HeadBranch: branchName,
479 BaseRepo: a.Config.GerritConfig.Project,
480 HeadRepo: a.Config.GerritConfig.Project,
481 }
482
483 pr, err := a.gitInterface.CreatePullRequest(ctx, prOptions)
484 if err != nil {
485 return fmt.Errorf("failed to create pull request: %w", err)
486 }
487
iomodo0c203b12025-07-26 19:44:57 +0400488 a.logger.Info("Created pull request for task", slog.String("id", task.ID), slog.String("title", pr.Title), slog.String("pr_id", pr.ID))
iomodo76f9a2d2025-07-26 12:14:40 +0400489 }
490
iomodo76f9a2d2025-07-26 12:14:40 +0400491 return nil
492}
493
494// generateBranchName generates a branch name for the task
495func (a *Agent) generateBranchName(task *tm.Task) string {
496 // Clean the task title for branch name
497 cleanTitle := strings.ReplaceAll(task.Title, " ", "-")
498 cleanTitle = strings.ToLower(cleanTitle)
499
500 // Remove special characters that are not allowed in git branch names
501 // Keep only alphanumeric characters and hyphens
502 var result strings.Builder
503 for _, char := range cleanTitle {
504 if (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char == '-' {
505 result.WriteRune(char)
506 }
507 }
508 cleanTitle = result.String()
509
510 // Remove consecutive hyphens
511 for strings.Contains(cleanTitle, "--") {
512 cleanTitle = strings.ReplaceAll(cleanTitle, "--", "-")
513 }
514
515 // Remove leading and trailing hyphens
516 cleanTitle = strings.Trim(cleanTitle, "-")
517
518 // Limit length
519 if len(cleanTitle) > 50 {
520 cleanTitle = cleanTitle[:50]
521 // Ensure we don't end with a hyphen after truncation
522 cleanTitle = strings.TrimSuffix(cleanTitle, "-")
523 }
524
525 return fmt.Sprintf("task/%s-%s", task.ID, cleanTitle)
526}
527
528// formatSolution formats the solution for the pull request
529func (a *Agent) formatSolution(task *tm.Task, solution string) string {
530 var content strings.Builder
531
532 content.WriteString(fmt.Sprintf("# Task Solution: %s\n\n", task.Title))
533 content.WriteString(fmt.Sprintf("**Task ID:** %s\n", task.ID))
534 content.WriteString(fmt.Sprintf("**Agent:** %s (%s)\n", a.Config.Name, a.Config.Role))
535 content.WriteString(fmt.Sprintf("**Completed:** %s\n\n", time.Now().Format("2006-01-02 15:04:05")))
536
537 content.WriteString("## Task Description\n\n")
538 content.WriteString(task.Description)
539 content.WriteString("\n\n")
540
541 content.WriteString("## Solution\n\n")
542 content.WriteString(solution)
543 content.WriteString("\n\n")
544
545 content.WriteString("---\n")
546 content.WriteString("*This solution was generated by AI Agent*\n")
547
548 return content.String()
549}
550
iomodo570d4262025-07-26 16:26:36 +0400551// formatPullRequestDescription formats the description for the pull request
552func (a *Agent) formatPullRequestDescription(task *tm.Task, solution string) string {
553 var content strings.Builder
554
555 content.WriteString(fmt.Sprintf("**Task ID:** %s\n", task.ID))
556 content.WriteString(fmt.Sprintf("**Title:** %s\n", task.Title))
557 content.WriteString(fmt.Sprintf("**Priority:** %s\n", task.Priority))
558
559 if task.Description != "" {
560 content.WriteString(fmt.Sprintf("**Description:** %s\n", task.Description))
561 }
562
563 if task.DueDate != nil {
564 content.WriteString(fmt.Sprintf("**Due Date:** %s\n", task.DueDate.Format("2006-01-02")))
565 }
566
567 content.WriteString("\n**Solution:**\n\n")
568 content.WriteString(solution)
569
570 return content.String()
571}
572
iomodo76f9a2d2025-07-26 12:14:40 +0400573// ptr helpers for cleaner code
574func intPtr(i int) *int {
575 return &i
576}
577
578func float64Ptr(f float64) *float64 {
579 return &f
iomodob67a3762025-07-25 20:27:04 +0400580}