Refactor Tender Management to Introduce Notice Entity and Repository
- Replaced the tender repository with a new notice repository, encapsulating notice-related data access methods. - Introduced the Notice entity to represent tender/contract notices, including relevant fields and methods for managing notice data. - Updated the TED scraper to utilize the new notice repository for creating and managing notices, enhancing the integration with the tender management system. - Implemented Ollama SDK initialization in the web bootstrap process, allowing for improved AI interactions. - Enhanced the tender service to include Ollama SDK for additional functionality, ensuring a more robust service layer.
This commit is contained in:
@@ -74,6 +74,21 @@ type NotificationConfig struct {
|
||||
UserAgent string `env:"NOTIFICATION_USER_AGENT"`
|
||||
}
|
||||
|
||||
// OllamaConfig represents the configuration for the Ollama SDK
|
||||
type OllamaConfig struct {
|
||||
BaseURL string `env:"OLLAMA_BASE_URL" default:"http://localhost:11434"`
|
||||
Timeout time.Duration `env:"OLLAMA_TIMEOUT" default:"300s"`
|
||||
RetryAttempts int `env:"OLLAMA_RETRY_ATTEMPTS" default:"3"`
|
||||
RetryDelay time.Duration `env:"OLLAMA_RETRY_DELAY" default:"2s"`
|
||||
EnableLogging bool `env:"OLLAMA_ENABLE_LOGGING" default:"true"`
|
||||
UserAgent string `env:"OLLAMA_USER_AGENT" default:"TenderManagement-OllamaSDK/1.0"`
|
||||
DefaultModel string `env:"OLLAMA_DEFAULT_MODEL" default:"llama2"`
|
||||
DefaultTemperature float64 `env:"OLLAMA_DEFAULT_TEMPERATURE" default:"0.7"`
|
||||
DefaultTopP float64 `env:"OLLAMA_DEFAULT_TOP_P" default:"0.9"`
|
||||
DefaultMaxTokens int `env:"OLLAMA_DEFAULT_MAX_TOKENS" default:"1000"`
|
||||
EnableStreaming bool `env:"OLLAMA_ENABLE_STREAMING" default:"false"`
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration with priority: OS environment variables > .env file
|
||||
// OS environment variables take precedence over .env file values
|
||||
func LoadConfig[T any](path string, config T) (T, error) {
|
||||
|
||||
@@ -47,6 +47,9 @@ type Repository[T any] interface {
|
||||
// Create inserts a new document
|
||||
Create(ctx context.Context, model *T) error
|
||||
|
||||
// CreateMany inserts multiple documents
|
||||
CreateMany(ctx context.Context, models []T) error
|
||||
|
||||
// Update updates an existing document
|
||||
Update(ctx context.Context, model *T) error
|
||||
|
||||
@@ -239,6 +242,32 @@ func (r *repository[T]) Create(ctx context.Context, model *T) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateMany inserts multiple documents
|
||||
func (r *repository[T]) CreateMany(ctx context.Context, models []T) error {
|
||||
// Set timestamps if the model implements Timestampable
|
||||
for _, v := range models {
|
||||
if timestampable, ok := any(v).(Timestampable); ok {
|
||||
now := time.Now().Unix()
|
||||
timestampable.SetCreatedAt(now)
|
||||
timestampable.SetUpdatedAt(now)
|
||||
}
|
||||
}
|
||||
|
||||
result, err := r.collection.InsertMany(ctx, models)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to create multiple documents", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return fmt.Errorf("failed to create multiple documents: %w", err)
|
||||
}
|
||||
|
||||
r.logger.Info("Documents created successfully", map[string]interface{}{
|
||||
"ids": result.InsertedIDs,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update updates an existing document
|
||||
func (r *repository[T]) Update(ctx context.Context, model *T) error {
|
||||
// Set updated timestamp if the model implements Timestampable
|
||||
|
||||
@@ -0,0 +1,444 @@
|
||||
package ollama
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
// Service implements the OllamaService interface
|
||||
type Service struct {
|
||||
client *Client
|
||||
config *Config
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewService creates a new Ollama service
|
||||
func NewService(client *Client) OllamaService {
|
||||
return &Service{
|
||||
client: client,
|
||||
config: client.config,
|
||||
logger: client.logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate generates text using a specified model and prompt
|
||||
func (s *Service) Generate(ctx context.Context, req *GenerateRequest) (*GenerateResponse, error) {
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Generating text", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"prompt_length": len(req.Prompt),
|
||||
})
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if req.Model == "" {
|
||||
req.Model = s.config.DefaultModel
|
||||
}
|
||||
|
||||
var resp GenerateResponse
|
||||
err := s.client.Post(ctx, "/api/generate", req, &resp)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Error("Text generation failed", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
return nil, ErrGenerationFailed(err.Error())
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Text generation completed", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"response_length": len(resp.Response),
|
||||
"eval_count": resp.EvalCount,
|
||||
})
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// GenerateWithOptions generates text with additional options
|
||||
func (s *Service) GenerateWithOptions(ctx context.Context, req *GenerateRequest, opts *GenerateOptions) (*GenerateResponse, error) {
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Generating text with options", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"prompt_length": len(req.Prompt),
|
||||
})
|
||||
}
|
||||
|
||||
// Apply options to request
|
||||
if opts != nil {
|
||||
if req.Options == nil {
|
||||
req.Options = make(map[string]interface{})
|
||||
}
|
||||
|
||||
if opts.Temperature != nil {
|
||||
req.Options["temperature"] = *opts.Temperature
|
||||
}
|
||||
if opts.TopP != nil {
|
||||
req.Options["top_p"] = *opts.TopP
|
||||
}
|
||||
if opts.TopK != nil {
|
||||
req.Options["top_k"] = *opts.TopK
|
||||
}
|
||||
if opts.RepeatPenalty != nil {
|
||||
req.Options["repeat_penalty"] = *opts.RepeatPenalty
|
||||
}
|
||||
if opts.Seed != nil {
|
||||
req.Options["seed"] = *opts.Seed
|
||||
}
|
||||
if opts.Stop != nil {
|
||||
req.Options["stop"] = opts.Stop
|
||||
}
|
||||
if opts.Format != "" {
|
||||
req.Format = opts.Format
|
||||
}
|
||||
|
||||
// Merge additional options
|
||||
for k, v := range opts.Options {
|
||||
req.Options[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return s.Generate(ctx, req)
|
||||
}
|
||||
|
||||
// Chat performs a conversational chat with the model
|
||||
func (s *Service) Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Starting chat", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"message_count": len(req.Messages),
|
||||
})
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if req.Model == "" {
|
||||
req.Model = s.config.DefaultModel
|
||||
}
|
||||
|
||||
var resp ChatResponse
|
||||
err := s.client.Post(ctx, "/api/chat", req, &resp)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Error("Chat failed", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
return nil, ErrChatFailed(err.Error())
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Chat completed", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"response_length": len(resp.Message.Content),
|
||||
"eval_count": resp.EvalCount,
|
||||
})
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// Embed generates embeddings for the given text
|
||||
func (s *Service) Embed(ctx context.Context, req *EmbedRequest) (*EmbedResponse, error) {
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Generating embeddings", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"text_length": len(req.Prompt),
|
||||
})
|
||||
}
|
||||
|
||||
var resp EmbedResponse
|
||||
err := s.client.Post(ctx, "/api/embeddings", req, &resp)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Error("Embedding generation failed", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
return nil, ErrEmbeddingFailed(err.Error())
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Embeddings generated", map[string]interface{}{
|
||||
"model": req.Model,
|
||||
"embedding_dimension": len(resp.Embedding),
|
||||
})
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// PullModel pulls a model from the registry
|
||||
func (s *Service) PullModel(ctx context.Context, model string) (*PullResponse, error) {
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Pulling model", map[string]interface{}{
|
||||
"model": model,
|
||||
})
|
||||
}
|
||||
|
||||
req := &PullRequest{
|
||||
Name: model,
|
||||
}
|
||||
|
||||
var resp PullResponse
|
||||
err := s.client.Post(ctx, "/api/pull", req, &resp)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Error("Model pull failed", map[string]interface{}{
|
||||
"model": model,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
return nil, ErrModelPullFailed(model, err.Error())
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Model pulled successfully", map[string]interface{}{
|
||||
"model": model,
|
||||
"status": resp.Status,
|
||||
})
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// ListModels lists available models
|
||||
func (s *Service) ListModels(ctx context.Context) (*ListModelsResponse, error) {
|
||||
if s.logger != nil {
|
||||
s.logger.Debug("Listing models", map[string]interface{}{})
|
||||
}
|
||||
|
||||
var resp ListModelsResponse
|
||||
err := s.client.Get(ctx, "/api/tags", &resp)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Error("Failed to list models", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
return nil, fmt.Errorf("failed to list models: %w", err)
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Debug("Models listed", map[string]interface{}{
|
||||
"model_count": len(resp.Models),
|
||||
})
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// DeleteModel deletes a model
|
||||
func (s *Service) DeleteModel(ctx context.Context, model string) (*DeleteResponse, error) {
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Deleting model", map[string]interface{}{
|
||||
"model": model,
|
||||
})
|
||||
}
|
||||
|
||||
req := &DeleteRequest{
|
||||
Name: model,
|
||||
}
|
||||
|
||||
var resp DeleteResponse
|
||||
err := s.client.Delete(ctx, "/api/delete", req, &resp)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Error("Model deletion failed", map[string]interface{}{
|
||||
"model": model,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
return nil, ErrModelDeleteFailed(model, err.Error())
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Model deleted successfully", map[string]interface{}{
|
||||
"model": model,
|
||||
"status": resp.Status,
|
||||
})
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// ShowModel shows model information
|
||||
func (s *Service) ShowModel(ctx context.Context, model string) (*ShowModelResponse, error) {
|
||||
if s.logger != nil {
|
||||
s.logger.Debug("Showing model information", map[string]interface{}{
|
||||
"model": model,
|
||||
})
|
||||
}
|
||||
|
||||
req := &ShowModelRequest{
|
||||
Name: model,
|
||||
}
|
||||
|
||||
var resp ShowModelResponse
|
||||
err := s.client.Post(ctx, "/api/show", req, &resp)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Error("Failed to show model", map[string]interface{}{
|
||||
"model": model,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
return nil, fmt.Errorf("failed to show model %s: %w", model, err)
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Debug("Model information retrieved", map[string]interface{}{
|
||||
"model": model,
|
||||
})
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// Health checks if the Ollama service is available
|
||||
func (s *Service) Health(ctx context.Context) error {
|
||||
if s.logger != nil {
|
||||
s.logger.Debug("Checking Ollama health", map[string]interface{}{})
|
||||
}
|
||||
|
||||
var resp HealthResponse
|
||||
err := s.client.Get(ctx, "/api/version", &resp)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Error("Health check failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
return ErrConnectionFailed(err.Error())
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Debug("Health check passed", map[string]interface{}{})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewChatBuilder creates a new chat builder for conversational interactions
|
||||
func (s *Service) NewChatBuilder() ChatBuilder {
|
||||
return &chatBuilder{
|
||||
service: s,
|
||||
request: &ChatRequest{
|
||||
Model: s.config.DefaultModel,
|
||||
Messages: make([]Message, 0),
|
||||
Options: ChatOptions{
|
||||
Temperature: &s.config.DefaultTemperature,
|
||||
TopP: &s.config.DefaultTopP,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// chatBuilder implements the ChatBuilder interface
|
||||
type chatBuilder struct {
|
||||
service OllamaService
|
||||
request *ChatRequest
|
||||
}
|
||||
|
||||
func (b *chatBuilder) SetModel(model string) ChatBuilder {
|
||||
b.request.Model = model
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chatBuilder) SetSystem(system string) ChatBuilder {
|
||||
// Add or update system message
|
||||
for i, msg := range b.request.Messages {
|
||||
if msg.Role == MessageRoleSystem {
|
||||
b.request.Messages[i].Content = system
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
// Add new system message at the beginning
|
||||
systemMsg := Message{
|
||||
Role: MessageRoleSystem,
|
||||
Content: system,
|
||||
}
|
||||
b.request.Messages = append([]Message{systemMsg}, b.request.Messages...)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chatBuilder) AddMessage(role MessageRole, content string) ChatBuilder {
|
||||
msg := Message{
|
||||
Role: role,
|
||||
Content: content,
|
||||
}
|
||||
b.request.Messages = append(b.request.Messages, msg)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chatBuilder) SetTemperature(temperature float64) ChatBuilder {
|
||||
b.request.Options.Temperature = &temperature
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chatBuilder) SetTopP(topP float64) ChatBuilder {
|
||||
b.request.Options.TopP = &topP
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chatBuilder) SetMaxTokens(maxTokens int) ChatBuilder {
|
||||
b.request.Options.NumPredict = &maxTokens
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chatBuilder) SetStream(stream bool) ChatBuilder {
|
||||
b.request.Stream = &stream
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chatBuilder) SetFormat(format string) ChatBuilder {
|
||||
b.request.Format = format
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chatBuilder) SetOptions(options map[string]interface{}) ChatBuilder {
|
||||
if b.request.Options.Options == nil {
|
||||
b.request.Options.Options = make(map[string]interface{})
|
||||
}
|
||||
for k, v := range options {
|
||||
b.request.Options.Options[k] = v
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chatBuilder) Build() (*ChatRequest, error) {
|
||||
if len(b.request.Messages) == 0 {
|
||||
return nil, ErrInvalidRequest("at least one message is required")
|
||||
}
|
||||
return b.request, nil
|
||||
}
|
||||
|
||||
func (b *chatBuilder) Send(ctx context.Context) (*ChatResponse, error) {
|
||||
req, err := b.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.service.Chat(ctx, req)
|
||||
}
|
||||
|
||||
func (b *chatBuilder) Continue(ctx context.Context, userMessage string) (*ChatResponse, error) {
|
||||
// Add user message
|
||||
b.AddMessage(MessageRoleUser, userMessage)
|
||||
|
||||
// Send chat request
|
||||
resp, err := b.Send(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add assistant response to conversation history
|
||||
b.AddMessage(MessageRoleAssistant, resp.Message.Content)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package schedule
|
||||
|
||||
import (
|
||||
"time"
|
||||
"tm/internal/tender"
|
||||
"tm/internal/notice"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/mongo"
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
type CronScheduler struct {
|
||||
cron *cron.Cron
|
||||
mongoManager *mongo.ConnectionManager
|
||||
tenderRepo tender.TenderRepository
|
||||
noticeRepo notice.Repository
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
@@ -31,14 +31,14 @@ type Job struct {
|
||||
}
|
||||
|
||||
// NewCronScheduler creates a new cron scheduler instance
|
||||
func NewCronScheduler(logger logger.Logger, mongoManager *mongo.ConnectionManager, tenderRepo tender.TenderRepository) *CronScheduler {
|
||||
func NewCronScheduler(logger logger.Logger, mongoManager *mongo.ConnectionManager, noticeRepo notice.Repository) *CronScheduler {
|
||||
// Create cron with timezone support and seconds precision
|
||||
c := cron.New(cron.WithLocation(time.UTC), cron.WithSeconds())
|
||||
|
||||
return &CronScheduler{
|
||||
cron: c,
|
||||
mongoManager: mongoManager,
|
||||
tenderRepo: tenderRepo,
|
||||
noticeRepo: noticeRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user