Add answer worker

Subsystem for message processing: parses messages, generates LLM
responses, and replies with SMTP.

Introduces:
- answer: message processing worker.
- llm: OpenAI API compatible client with support for tool execution.
- message: message parsing and response logic.
- tool: converts YAML configuration into executable subprocesses.
- smtp: simple config and client wrapper for sending email.
This commit is contained in:
dwrz
2026-01-04 21:01:57 +00:00
parent c53ee5f6ad
commit 486cc5fa52
9 changed files with 3228 additions and 0 deletions

66
internal/smtp/smtp.go Normal file
View File

@@ -0,0 +1,66 @@
// Package smtp provides a config wrapper around net/smtp for sending email.
package smtp
import (
"fmt"
"net/mail"
"net/smtp"
)
// SMTP holds configuration for sending emails via an SMTP server.
type SMTP struct {
From string `yaml:"from"`
Host string `yaml:"host"`
Password string `yaml:"password"`
Port string `yaml:"port"`
User string `yaml:"user"`
}
// Address returns the host:port string.
func (s *SMTP) Address() string {
return fmt.Sprintf("%s:%s", s.Host, s.Port)
}
// Auth creates a PlainAuth using the configured credentials.
func (s *SMTP) Auth() smtp.Auth {
return smtp.PlainAuth("", s.User, s.Password, s.Host)
}
// Send emails the given recipients using net/smtp.
// Refer to smtp.SendMail for its parameters and limitations.
func (s *SMTP) Send(to []string, msg []byte) error {
if err := smtp.SendMail(
s.Address(),
s.Auth(),
s.From,
to,
msg,
); err != nil {
return fmt.Errorf("smtp send: %w", err)
}
return nil
}
// Validate checks that all required configuration fields are set.
func (s *SMTP) Validate() error {
if s.From == "" {
return fmt.Errorf("missing from")
}
if _, err := mail.ParseAddress(s.From); err != nil {
return fmt.Errorf("invalid from: %w", err)
}
if s.Host == "" {
return fmt.Errorf("missing host")
}
if s.Password == "" {
return fmt.Errorf("missing password")
}
if s.Port == "" {
return fmt.Errorf("missing port")
}
if s.User == "" {
return fmt.Errorf("missing user")
}
return nil
}

111
internal/smtp/smtp_test.go Normal file
View File

@@ -0,0 +1,111 @@
package smtp
import (
"testing"
)
func TestAddress(t *testing.T) {
s := &SMTP{
Host: "smtp.example.com",
Port: "587",
}
expected := "smtp.example.com:587"
if got := s.Address(); got != expected {
t.Errorf("Address() = %q, want %q", got, expected)
}
}
func TestAuth(t *testing.T) {
s := &SMTP{
User: "user",
Password: "password",
Host: "smtp.example.com",
}
// Verify return of non-nil Auth mechanism.
if got := s.Auth(); got == nil {
t.Error("Auth() returned nil")
}
}
func TestValidate(t *testing.T) {
tests := []struct {
name string
s *SMTP
shouldError bool
}{
{
name: "valid configuration",
s: &SMTP{
From: "sender@example.com",
Host: "smtp.example.com",
Password: "secretpassword",
Port: "587",
User: "user",
},
shouldError: false,
},
{
name: "missing from",
s: &SMTP{
Host: "smtp.example.com",
Password: "p",
Port: "587",
User: "u",
},
shouldError: true,
},
{
name: "missing host",
s: &SMTP{
From: "f",
Password: "p",
Port: "587",
User: "u",
},
shouldError: true,
},
{
name: "missing password",
s: &SMTP{
From: "f",
Host: "h",
Port: "587",
User: "u",
},
shouldError: true,
},
{
name: "missing port",
s: &SMTP{
From: "f",
Host: "h",
Password: "p",
User: "u",
},
shouldError: true,
},
{
name: "missing user",
s: &SMTP{
From: "f",
Host: "h",
Password: "p",
Port: "587",
},
shouldError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.s.Validate()
if tt.shouldError && err == nil {
t.Fatalf("expected error, got nil")
}
if !tt.shouldError && err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}