get started
This commit is contained in:
+454
@@ -0,0 +1,454 @@
|
||||
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
tests: false
|
||||
|
||||
|
||||
issues:
|
||||
# Maximum count of issues with the same text.
|
||||
# Set to 0 to disable.
|
||||
# Default: 3
|
||||
max-same-issues: 50
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- goimports # checks if the code and import statements are formatted according to the 'goimports' command
|
||||
#- golines # checks if code is formatted, and fixes long lines
|
||||
|
||||
## you may want to enable
|
||||
#- gci # checks if code and import statements are formatted, with additional rules
|
||||
- gofmt # checks if the code is formatted according to 'gofmt' command
|
||||
- gofumpt # enforces a stricter format than 'gofmt', while being backwards compatible
|
||||
#- swaggo # formats swaggo comments
|
||||
|
||||
# All settings can be found here https://github.com/golangci/golangci-lint/blob/HEAD/.golangci.reference.yml
|
||||
settings:
|
||||
goimports:
|
||||
# A list of prefixes, which, if set, checks import paths
|
||||
# with the given prefixes are grouped after 3rd-party packages.
|
||||
# Default: []
|
||||
local-prefixes:
|
||||
- nf-api
|
||||
|
||||
golines:
|
||||
# Target maximum line length.
|
||||
# Default: 100
|
||||
max-len: 120
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- asasalint # checks for pass []any as any in variadic func(...any)
|
||||
- asciicheck # checks that your code does not contain non-ASCII identifiers
|
||||
- bidichk # checks for dangerous unicode character sequences
|
||||
- bodyclose # checks whether HTTP response body is closed successfully
|
||||
- canonicalheader # checks whether net/http.Header uses canonical header
|
||||
- copyloopvar # detects places where loop variables are copied (Go 1.22+)
|
||||
- cyclop # checks function and package cyclomatic complexity
|
||||
- depguard # checks if package imports are in a list of acceptable packages
|
||||
- dupl # tool for code clone detection
|
||||
- durationcheck # checks for two durations multiplied together
|
||||
- embeddedstructfieldcheck # checks embedded types in structs
|
||||
- errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases
|
||||
- errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error
|
||||
- errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13
|
||||
- exhaustive # checks exhaustiveness of enum switch statements
|
||||
- exptostd # detects functions from golang.org/x/exp/ that can be replaced by std functions
|
||||
- fatcontext # detects nested contexts in loops
|
||||
- forbidigo # forbids identifiers
|
||||
- funcorder # checks the order of functions, methods, and constructors
|
||||
- funlen # tool for detection of long functions
|
||||
- gocheckcompilerdirectives # validates go compiler directive comments (//go:)
|
||||
- gochecknoglobals # checks that no global variables exist
|
||||
- gochecknoinits # checks that no init functions are present in Go code
|
||||
- gochecksumtype # checks exhaustiveness on Go "sum types"
|
||||
- gocognit # computes and checks the cognitive complexity of functions
|
||||
- goconst # finds repeated strings that could be replaced by a constant
|
||||
- gocritic # provides diagnostics that check for bugs, performance and style issues
|
||||
- gocyclo # computes and checks the cyclomatic complexity of functions
|
||||
- godot # checks if comments end in a period
|
||||
- gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod
|
||||
- goprintffuncname # checks that printf-like functions are named with f at the end
|
||||
- gosec # inspects source code for security problems
|
||||
- govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
|
||||
- iface # checks the incorrect use of interfaces, helping developers avoid interface pollution
|
||||
- ineffassign # detects when assignments to existing variables are not used
|
||||
- intrange # finds places where for loops could make use of an integer range
|
||||
- loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap)
|
||||
- makezero # finds slice declarations with non-zero initial length
|
||||
- mirror # reports wrong mirror patterns of bytes/strings usage
|
||||
- mnd # detects magic numbers
|
||||
- musttag # enforces field tags in (un)marshaled structs
|
||||
- nakedret # finds naked returns in functions greater than a specified function length
|
||||
- nestif # reports deeply nested if statements
|
||||
- nilerr # finds the code that returns nil even if it checks that the error is not nil
|
||||
- nilnesserr # reports that it checks for err != nil, but it returns a different nil value error (powered by nilness and nilerr)
|
||||
- nilnil # checks that there is no simultaneous return of nil error and an invalid value
|
||||
- noctx # finds sending http request without context.Context
|
||||
- nolintlint # reports ill-formed or insufficient nolint directives
|
||||
- nonamedreturns # reports all named returns
|
||||
- nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL
|
||||
- perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative
|
||||
- predeclared # finds code that shadows one of Go's predeclared identifiers
|
||||
- promlinter # checks Prometheus metrics naming via promlint
|
||||
- protogetter # reports direct reads from proto message fields when getters should be used
|
||||
- reassign # checks that package variables are not reassigned
|
||||
- recvcheck # checks for receiver type consistency
|
||||
- revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint
|
||||
- rowserrcheck # checks whether Err of rows is checked successfully
|
||||
# - sloglint # ensure consistent code style when using log/slog
|
||||
- spancheck # checks for mistakes with OpenTelemetry/Census spans
|
||||
- sqlclosecheck # checks that sql.Rows and sql.Stmt are closed
|
||||
- staticcheck # is a go vet on steroids, applying a ton of static analysis checks
|
||||
- testableexamples # checks if examples are testable (have an expected output)
|
||||
- testifylint # checks usage of github.com/stretchr/testify
|
||||
- testpackage # makes you use a separate _test package
|
||||
- tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes
|
||||
- unconvert # removes unnecessary type conversions
|
||||
- unparam # reports unused function parameters
|
||||
- unused # checks for unused constants, variables, functions and types
|
||||
- usestdlibvars # detects the possibility to use variables/constants from the Go standard library
|
||||
- usetesting # reports uses of functions with replacement inside the testing package
|
||||
- wastedassign # finds wasted assignment statements
|
||||
- whitespace # detects leading and trailing whitespace
|
||||
|
||||
## you may want to enable
|
||||
#- arangolint # opinionated best practices for arangodb client
|
||||
#- decorder # checks declaration order and count of types, constants, variables and functions
|
||||
#- exhaustruct # [highly recommend to enable] checks if all structure fields are initialized
|
||||
#- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega
|
||||
#- godox # detects usage of FIXME, TODO and other keywords inside comments
|
||||
#- goheader # checks is file header matches to pattern
|
||||
#- inamedparam # [great idea, but too strict, need to ignore a lot of cases by default] reports interfaces with unnamed method parameters
|
||||
#- interfacebloat # checks the number of methods inside an interface
|
||||
#- ireturn # accept interfaces, return concrete types
|
||||
#- noinlineerr # disallows inline error handling `if err := ...; err != nil {`
|
||||
#- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated
|
||||
#- tagalign # checks that struct tags are well aligned
|
||||
#- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope
|
||||
#- wrapcheck # checks that errors returned from external packages are wrapped
|
||||
#- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event
|
||||
|
||||
## disabled
|
||||
#- containedctx # detects struct contained context.Context field
|
||||
#- contextcheck # [too many false positives] checks the function whether use a non-inherited context
|
||||
#- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
|
||||
#- dupword # [useless without config] checks for duplicate words in the source code
|
||||
#- err113 # [too strict] checks the errors handling expressions
|
||||
#- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted
|
||||
#- forcetypeassert # [replaced by errcheck] finds forced type assertions
|
||||
#- gomodguard # [use more powerful depguard] allow and block lists linter for direct Go module dependencies
|
||||
#- gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase
|
||||
#- grouper # analyzes expression groups
|
||||
#- importas # enforces consistent import aliases
|
||||
#- lll # [replaced by golines] reports long lines
|
||||
#- maintidx # measures the maintainability index of each function
|
||||
#- misspell # [useless] finds commonly misspelled English words in comments
|
||||
#- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity
|
||||
#- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test
|
||||
#- tagliatelle # checks the struct tags
|
||||
#- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers
|
||||
#- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines
|
||||
#- wsl_v5 # [too strict and mostly code is not more readable] add or remove empty lines
|
||||
|
||||
# All settings can be found here https://github.com/golangci/golangci-lint/blob/HEAD/.golangci.reference.yml
|
||||
settings:
|
||||
cyclop:
|
||||
# The maximal code complexity to report.
|
||||
# Default: 10
|
||||
max-complexity: 30
|
||||
# The maximal average package complexity.
|
||||
# If it's higher than 0.0 (float) the check is enabled.
|
||||
# Default: 0.0
|
||||
package-average: 10.0
|
||||
|
||||
depguard:
|
||||
# Rules to apply.
|
||||
#
|
||||
# Variables:
|
||||
# - File Variables
|
||||
# Use an exclamation mark `!` to negate a variable.
|
||||
# Example: `!$test` matches any file that is not a go test file.
|
||||
#
|
||||
# `$all` - matches all go files
|
||||
# `$test` - matches all go test files
|
||||
#
|
||||
# - Package Variables
|
||||
#
|
||||
# `$gostd` - matches all of go's standard library (Pulled from `GOROOT`)
|
||||
#
|
||||
# Default (applies if no custom rules are defined): Only allow $gostd in all files.
|
||||
rules:
|
||||
"deprecated":
|
||||
# List of file globs that will match this list of settings to compare against.
|
||||
# By default, if a path is relative, it is relative to the directory where the golangci-lint command is executed.
|
||||
# The placeholder '${base-path}' is substituted with a path relative to the mode defined with `run.relative-path-mode`.
|
||||
# The placeholder '${config-path}' is substituted with a path relative to the configuration file.
|
||||
# Default: $all
|
||||
files:
|
||||
- "$all"
|
||||
# List of packages that are not allowed.
|
||||
# Entries can be a variable (starting with $), a string prefix, or an exact match (if ending with $).
|
||||
# Default: []
|
||||
deny:
|
||||
- pkg: github.com/golang/protobuf
|
||||
desc: Use google.golang.org/protobuf instead, see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules
|
||||
- pkg: github.com/satori/go.uuid
|
||||
desc: Use github.com/google/uuid instead, satori's package is not maintained
|
||||
- pkg: github.com/gofrs/uuid$
|
||||
desc: Use github.com/gofrs/uuid/v5 or later, it was not a go module before v5
|
||||
"non-test files":
|
||||
files:
|
||||
- "!$test"
|
||||
deny:
|
||||
- pkg: math/rand$
|
||||
desc: Use math/rand/v2 instead, see https://go.dev/blog/randv2
|
||||
"non-main files":
|
||||
files:
|
||||
- "!**/main.go"
|
||||
deny:
|
||||
- pkg: log$
|
||||
desc: Use log/slog instead, see https://go.dev/blog/slog
|
||||
|
||||
embeddedstructfieldcheck:
|
||||
# Checks that sync.Mutex and sync.RWMutex are not used as embedded fields.
|
||||
# Default: false
|
||||
forbid-mutex: true
|
||||
|
||||
errcheck:
|
||||
# Report about not checking of errors in type assertions: `a := b.(MyStruct)`.
|
||||
# Such cases aren't reported by default.
|
||||
# Default: false
|
||||
check-type-assertions: true
|
||||
|
||||
exhaustive:
|
||||
# Program elements to check for exhaustiveness.
|
||||
# Default: [ switch ]
|
||||
check:
|
||||
- switch
|
||||
- map
|
||||
|
||||
exhaustruct:
|
||||
# List of regular expressions to match type names that should be excluded from processing.
|
||||
# Anonymous structs can be matched by '<anonymous>' alias.
|
||||
# Has precedence over `include`.
|
||||
# Each regular expression must match the full type name, including package path.
|
||||
# For example, to match type `net/http.Cookie` regular expression should be `.*/http\.Cookie`,
|
||||
# but not `http\.Cookie`.
|
||||
# Default: []
|
||||
exclude:
|
||||
# std libs
|
||||
- ^net/http.Client$
|
||||
- ^net/http.Cookie$
|
||||
- ^net/http.Request$
|
||||
- ^net/http.Response$
|
||||
- ^net/http.Server$
|
||||
- ^net/http.Transport$
|
||||
- ^net/url.URL$
|
||||
- ^os/exec.Cmd$
|
||||
- ^reflect.StructField$
|
||||
# public libs
|
||||
- ^github.com/Shopify/sarama.Config$
|
||||
- ^github.com/Shopify/sarama.ProducerMessage$
|
||||
- ^github.com/mitchellh/mapstructure.DecoderConfig$
|
||||
- ^github.com/prometheus/client_golang/.+Opts$
|
||||
- ^github.com/spf13/cobra.Command$
|
||||
- ^github.com/spf13/cobra.CompletionOptions$
|
||||
- ^github.com/stretchr/testify/mock.Mock$
|
||||
- ^github.com/testcontainers/testcontainers-go.+Request$
|
||||
- ^github.com/testcontainers/testcontainers-go.FromDockerfile$
|
||||
- ^golang.org/x/tools/go/analysis.Analyzer$
|
||||
- ^google.golang.org/protobuf/.+Options$
|
||||
- ^gopkg.in/yaml.v3.Node$
|
||||
# Allows empty structures in return statements.
|
||||
# Default: false
|
||||
allow-empty-returns: true
|
||||
|
||||
funcorder:
|
||||
# Checks if the exported methods of a structure are placed before the non-exported ones.
|
||||
# Default: true
|
||||
struct-method: false
|
||||
|
||||
funlen:
|
||||
# Checks the number of lines in a function.
|
||||
# If lower than 0, disable the check.
|
||||
# Default: 60
|
||||
lines: 100
|
||||
# Checks the number of statements in a function.
|
||||
# If lower than 0, disable the check.
|
||||
# Default: 40
|
||||
statements: 50
|
||||
|
||||
gochecksumtype:
|
||||
# Presence of `default` case in switch statements satisfies exhaustiveness, if all members are not listed.
|
||||
# Default: true
|
||||
default-signifies-exhaustive: false
|
||||
|
||||
gocognit:
|
||||
# Minimal code complexity to report.
|
||||
# Default: 30 (but we recommend 10-20)
|
||||
min-complexity: 20
|
||||
|
||||
gocritic:
|
||||
# Settings passed to gocritic.
|
||||
# The settings key is the name of a supported gocritic checker.
|
||||
# The list of supported checkers can be found at https://go-critic.com/overview.
|
||||
settings:
|
||||
captLocal:
|
||||
# Whether to restrict checker to params only.
|
||||
# Default: true
|
||||
paramsOnly: false
|
||||
underef:
|
||||
# Whether to skip (*x).method() calls where x is a pointer receiver.
|
||||
# Default: true
|
||||
skipRecvDeref: false
|
||||
|
||||
govet:
|
||||
# Enable all analyzers.
|
||||
# Default: false
|
||||
enable-all: true
|
||||
# Disable analyzers by name.
|
||||
# Run `GL_DEBUG=govet golangci-lint run --enable=govet` to see default, all available analyzers, and enabled analyzers.
|
||||
# Default: []
|
||||
disable:
|
||||
- fieldalignment # too strict
|
||||
# Settings per analyzer.
|
||||
settings:
|
||||
shadow:
|
||||
# Whether to be strict about shadowing; can be noisy.
|
||||
# Default: false
|
||||
strict: true
|
||||
|
||||
inamedparam:
|
||||
# Skips check for interface methods with only a single parameter.
|
||||
# Default: false
|
||||
skip-single-param: true
|
||||
|
||||
mnd:
|
||||
# List of function patterns to exclude from analysis.
|
||||
# Values always ignored: `time.Date`,
|
||||
# `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`,
|
||||
# `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`.
|
||||
# Default: []
|
||||
ignored-functions:
|
||||
- args.Error
|
||||
- flag.Arg
|
||||
- flag.Duration.*
|
||||
- flag.Float.*
|
||||
- flag.Int.*
|
||||
- flag.Uint.*
|
||||
- os.Chmod
|
||||
- os.Mkdir.*
|
||||
- os.OpenFile
|
||||
- os.WriteFile
|
||||
- prometheus.ExponentialBuckets.*
|
||||
- prometheus.LinearBuckets
|
||||
|
||||
nakedret:
|
||||
# Make an issue if func has more lines of code than this setting, and it has naked returns.
|
||||
# Default: 30
|
||||
max-func-lines: 0
|
||||
|
||||
nolintlint:
|
||||
# Exclude following linters from requiring an explanation.
|
||||
# Default: []
|
||||
allow-no-explanation: [ funlen, gocognit, golines ]
|
||||
# Enable to require an explanation of nonzero length after each nolint directive.
|
||||
# Default: false
|
||||
require-explanation: true
|
||||
# Enable to require nolint directives to mention the specific linter being suppressed.
|
||||
# Default: false
|
||||
require-specific: true
|
||||
|
||||
perfsprint:
|
||||
# Optimizes into strings concatenation.
|
||||
# Default: true
|
||||
strconcat: false
|
||||
|
||||
reassign:
|
||||
# Patterns for global variable names that are checked for reassignment.
|
||||
# See https://github.com/curioswitch/go-reassign#usage
|
||||
# Default: ["EOF", "Err.*"]
|
||||
patterns:
|
||||
- ".*"
|
||||
|
||||
rowserrcheck:
|
||||
# database/sql is always checked.
|
||||
# Default: []
|
||||
packages:
|
||||
- github.com/jmoiron/sqlx
|
||||
|
||||
sloglint:
|
||||
# Enforce not using global loggers.
|
||||
# Values:
|
||||
# - "": disabled
|
||||
# - "all": report all global loggers
|
||||
# - "default": report only the default slog logger
|
||||
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-global
|
||||
# Default: ""
|
||||
no-global: all
|
||||
# Enforce using methods that accept a context.
|
||||
# Values:
|
||||
# - "": disabled
|
||||
# - "all": report all contextless calls
|
||||
# - "scope": report only if a context exists in the scope of the outermost function
|
||||
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#context-only
|
||||
# Default: ""
|
||||
context: scope
|
||||
|
||||
staticcheck:
|
||||
# SAxxxx checks in https://staticcheck.dev/docs/configuration/options/#checks
|
||||
# Example (to disable some checks): [ "all", "-SA1000", "-SA1001"]
|
||||
# Default: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022"]
|
||||
checks:
|
||||
- all
|
||||
# Incorrect or missing package comment.
|
||||
# https://staticcheck.dev/docs/checks/#ST1000
|
||||
- -ST1000
|
||||
# Use consistent method receiver names.
|
||||
# https://staticcheck.dev/docs/checks/#ST1016
|
||||
- -ST1016
|
||||
# Omit embedded fields from selector expression.
|
||||
# https://staticcheck.dev/docs/checks/#QF1008
|
||||
- -QF1008
|
||||
|
||||
usetesting:
|
||||
# Enable/disable `os.TempDir()` detections.
|
||||
# Default: false
|
||||
os-temp-dir: true
|
||||
|
||||
exclusions:
|
||||
# Log a warning if an exclusion rule is unused.
|
||||
# Default: false
|
||||
warn-unused: true
|
||||
# Predefined exclusion rules.
|
||||
# Default: []
|
||||
paths:
|
||||
- gen
|
||||
presets:
|
||||
- std-error-handling
|
||||
- common-false-positives
|
||||
# Excluding configuration per-path, per-linter, per-text and per-source.
|
||||
rules:
|
||||
- source: 'TODO'
|
||||
linters: [ godot ]
|
||||
- text: 'should have a package comment'
|
||||
linters: [ revive ]
|
||||
- text: 'exported \S+ \S+ should have comment( \(or a comment on this block\))? or be unexported'
|
||||
linters: [ revive ]
|
||||
- text: 'package comment should be of the form ".+"'
|
||||
source: '// ?(nolint|TODO)'
|
||||
linters: [ revive ]
|
||||
- text: 'comment on exported \S+ \S+ should be of the form ".+"'
|
||||
source: '// ?(nolint|TODO)'
|
||||
linters: [ revive, staticcheck ]
|
||||
- path: '_test\.go'
|
||||
linters:
|
||||
- bodyclose
|
||||
- dupl
|
||||
- errcheck
|
||||
- funlen
|
||||
- goconst
|
||||
- gosec
|
||||
- noctx
|
||||
- wrapcheck
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# Builder stage
|
||||
FROM golang:1.25-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o jules ./cmd/jules
|
||||
|
||||
# Runtime stage
|
||||
FROM alpine:latest
|
||||
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/jules .
|
||||
RUN chown -R appuser:appgroup /app
|
||||
USER appuser
|
||||
CMD ["./jules"]
|
||||
@@ -0,0 +1,29 @@
|
||||
|
||||
install:
|
||||
go install mvdan.cc/gofumpt@latest
|
||||
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||
go install github.com/go-bindata/go-bindata/...@latest
|
||||
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
||||
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
|
||||
|
||||
gen:
|
||||
mkdir -p gen/migrations
|
||||
go-bindata -pkg migrations -o gen/migrations/migrations.go -prefix "sql/migrations/" sql/migrations/...
|
||||
# mkdir -p gen/pb
|
||||
# protoc -I proto \
|
||||
# --go_out=gen/pb \
|
||||
# --go_opt=paths=source_relative \
|
||||
# --go-grpc_out=gen/pb \
|
||||
# --go-grpc_opt=paths=source_relative \
|
||||
# proto/jules.proto
|
||||
|
||||
fmt:
|
||||
gofumpt -w .
|
||||
|
||||
lint:
|
||||
golangci-lint run
|
||||
|
||||
test:
|
||||
go test -timeout=30s -count=1 -cover ./...
|
||||
@@ -1,2 +1,3 @@
|
||||
# jules
|
||||
Goal-pushing AI-agent.
|
||||
# Jules
|
||||
|
||||
Life-changing AI-assistant.
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package chat
|
||||
|
||||
import "context"
|
||||
|
||||
// Message represents an incoming message from a user.
|
||||
// ID is a universal user identifier (Telegram chat ID, email, phone, etc).
|
||||
type Message struct {
|
||||
ID string
|
||||
Text string
|
||||
}
|
||||
|
||||
// Chat defines the complete chat behavior.
|
||||
type Chat interface {
|
||||
// Send delivers a text message to the user identified by ID.
|
||||
Send(ctx context.Context, id string, text string) error
|
||||
|
||||
// Receive returns a channel that emits incoming messages.
|
||||
// The channel is closed when the context is cancelled or the underlying connection fails.
|
||||
Receive(ctx context.Context) (<-chan Message, error)
|
||||
|
||||
// Close gracefully shuts down the chat connection and releases resources.
|
||||
Close() error
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/d1nch8g/jules/chat"
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
const BaseURL = tgbotapi.APIEndpoint
|
||||
|
||||
type Bot struct {
|
||||
api *tgbotapi.BotAPI
|
||||
updates tgbotapi.UpdatesChannel
|
||||
}
|
||||
|
||||
func New(token, baseURL string) (*Bot, error) {
|
||||
client := &http.Client{}
|
||||
api, err := tgbotapi.NewBotAPIWithClient(token, baseURL, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Bot{
|
||||
api: api,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *Bot) Send(_ context.Context, id string, text string) error {
|
||||
chatID, err := strconv.ParseInt(id, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = b.api.Send(tgbotapi.NewMessage(chatID, text))
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *Bot) Receive(ctx context.Context) (<-chan chat.Message, error) {
|
||||
if b.updates != nil {
|
||||
return nil, errors.New("receive already started")
|
||||
}
|
||||
|
||||
b.updates = b.api.GetUpdatesChan(tgbotapi.NewUpdate(0))
|
||||
|
||||
out := make(chan chat.Message)
|
||||
go func() {
|
||||
defer close(out)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
b.api.StopReceivingUpdates()
|
||||
return
|
||||
case update := <-b.updates:
|
||||
if update.Message != nil {
|
||||
out <- chat.Message{
|
||||
ID: strconv.FormatInt(update.Message.Chat.ID, 10),
|
||||
Text: update.Message.Text,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (b *Bot) Close() error {
|
||||
if b.api != nil {
|
||||
b.api.StopReceivingUpdates()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type mockTelegramServer struct {
|
||||
mu sync.Mutex
|
||||
messages []mockMessage
|
||||
updateID int64
|
||||
}
|
||||
|
||||
type mockMessage struct {
|
||||
chatID int64
|
||||
text string
|
||||
}
|
||||
|
||||
func (m *mockTelegramServer) addMessage(chatID int64, text string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.messages = append(m.messages, mockMessage{chatID: chatID, text: text})
|
||||
}
|
||||
|
||||
func (m *mockTelegramServer) handler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/bottest_token/getMe":
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"ok": true,
|
||||
"result": map[string]string{"username": "test_bot"},
|
||||
})
|
||||
|
||||
case "/bottest_token/sendMessage":
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"ok": true,
|
||||
"result": map[string]any{"message_id": 1},
|
||||
})
|
||||
|
||||
case "/bottest_token/getUpdates":
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
updates := []map[string]any{}
|
||||
for _, msg := range m.messages {
|
||||
m.updateID++
|
||||
updates = append(updates, map[string]any{
|
||||
"update_id": m.updateID,
|
||||
"message": map[string]any{
|
||||
"message_id": m.updateID,
|
||||
"chat": map[string]any{"id": msg.chatID},
|
||||
"text": msg.text,
|
||||
},
|
||||
})
|
||||
}
|
||||
m.messages = nil
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"ok": true,
|
||||
"result": updates,
|
||||
})
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newMockServer() (*httptest.Server, *mockTelegramServer) {
|
||||
mock := &mockTelegramServer{}
|
||||
return httptest.NewServer(mock.handler()), mock
|
||||
}
|
||||
|
||||
func TestNew_MockAPI(t *testing.T) {
|
||||
server, _ := newMockServer()
|
||||
defer server.Close()
|
||||
|
||||
bot, err := New("test_token", server.URL+"/bot%s/%s")
|
||||
if err != nil {
|
||||
t.Fatalf("New failed: %v", err)
|
||||
}
|
||||
if bot == nil {
|
||||
t.Fatal("expected bot, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSend_MockAPI(t *testing.T) {
|
||||
server, _ := newMockServer()
|
||||
defer server.Close()
|
||||
|
||||
bot, err := New("test_token", server.URL+"/bot%s/%s")
|
||||
if err != nil {
|
||||
t.Fatalf("New failed: %v", err)
|
||||
}
|
||||
|
||||
err = bot.Send(t.Context(), "123456789", "hello")
|
||||
if err != nil {
|
||||
t.Errorf("Send failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSend_InvalidID(t *testing.T) {
|
||||
bot := &Bot{api: nil}
|
||||
|
||||
err := bot.Send(t.Context(), "not_a_number", "test")
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid id")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReceive_MockAPI(t *testing.T) {
|
||||
server, mock := newMockServer()
|
||||
defer server.Close()
|
||||
|
||||
bot, err := New("test_token", server.URL+"/bot%s/%s")
|
||||
if err != nil {
|
||||
t.Fatalf("New failed: %v", err)
|
||||
}
|
||||
|
||||
mock.addMessage(123456789, "hello world")
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
msgs, err := bot.Receive(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Receive failed: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case msg := <-msgs:
|
||||
if msg.ID != "123456789" {
|
||||
t.Errorf("expected ID '123456789', got '%s'", msg.ID)
|
||||
}
|
||||
if msg.Text != "hello world" {
|
||||
t.Errorf("expected text 'hello world', got '%s'", msg.Text)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReceive_AlreadyStarted(t *testing.T) {
|
||||
server, _ := newMockServer()
|
||||
defer server.Close()
|
||||
|
||||
bot, err := New("test_token", server.URL+"/bot%s/%s")
|
||||
if err != nil {
|
||||
t.Fatalf("New failed: %v", err)
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
_, err = bot.Receive(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("first Receive failed: %v", err)
|
||||
}
|
||||
|
||||
_, err = bot.Receive(ctx)
|
||||
if err == nil {
|
||||
t.Error("expected error when receive already started")
|
||||
}
|
||||
if err.Error() != "receive already started" {
|
||||
t.Errorf("expected 'receive already started', got '%s'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
server, _ := newMockServer()
|
||||
defer server.Close()
|
||||
|
||||
bot, err := New("test_token", server.URL+"/bot%s/%s")
|
||||
if err != nil {
|
||||
t.Fatalf("New failed: %v", err)
|
||||
}
|
||||
|
||||
err = bot.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClose_NilAPI(t *testing.T) {
|
||||
bot := &Bot{api: nil}
|
||||
|
||||
err := bot.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close with nil API failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewError(t *testing.T) {
|
||||
server, _ := newMockServer()
|
||||
defer server.Close()
|
||||
|
||||
_, err := New("test_token", server.URL+"/abbabaga")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/d1nch8g/jules/chat/telegram"
|
||||
"github.com/d1nch8g/jules/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
exit("failed to initialize configuration: %v", err)
|
||||
}
|
||||
|
||||
telegram, err := telegram.New(cfg.TelegramBotToken, telegram.BaseURL)
|
||||
if err != nil {
|
||||
exit("failed to initialize telegram: %v", err)
|
||||
}
|
||||
|
||||
_ = telegram
|
||||
}
|
||||
|
||||
func exit(msg string, args ...any) {
|
||||
slog.Error(msg, args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
TelegramBotToken string
|
||||
PostgresConnectionString string
|
||||
DeepSeekAPIKey string
|
||||
DeepSeekBaseURL string
|
||||
BraveAPIKey string
|
||||
}
|
||||
|
||||
func Load() (*Config, error) {
|
||||
cfg := &Config{
|
||||
TelegramBotToken: os.Getenv("TELEGRAM_BOT_TOKEN"),
|
||||
DeepSeekAPIKey: os.Getenv("DEEPSEEK_API_KEY"),
|
||||
BraveAPIKey: os.Getenv("BRAVE_API_KEY"),
|
||||
PostgresConnectionString: getEnv("POSTGRES_CONNECTION", "postgres://user:password@localhost:5432/db?sslmode=disable"),
|
||||
DeepSeekBaseURL: getEnv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
|
||||
}
|
||||
|
||||
if cfg.TelegramBotToken == "" {
|
||||
return nil, errors.New("TELEGRAM_BOT_TOKEN is required")
|
||||
}
|
||||
if cfg.DeepSeekAPIKey == "" {
|
||||
return nil, errors.New("DEEPSEEK_API_KEY is required")
|
||||
}
|
||||
if cfg.BraveAPIKey == "" {
|
||||
return nil, errors.New("BRAVE_API_KEY is required")
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "all required set",
|
||||
env: map[string]string{
|
||||
"TELEGRAM_BOT_TOKEN": "tg",
|
||||
"DEEPSEEK_API_KEY": "ds",
|
||||
"BRAVE_API_KEY": "br",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "missing telegram token",
|
||||
env: map[string]string{
|
||||
"DEEPSEEK_API_KEY": "ds",
|
||||
"BRAVE_API_KEY": "br",
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "TELEGRAM_BOT_TOKEN is required",
|
||||
},
|
||||
{
|
||||
name: "missing deepseek key",
|
||||
env: map[string]string{
|
||||
"TELEGRAM_BOT_TOKEN": "tg",
|
||||
"BRAVE_API_KEY": "br",
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "DEEPSEEK_API_KEY is required",
|
||||
},
|
||||
{
|
||||
name: "missing brave key",
|
||||
env: map[string]string{
|
||||
"TELEGRAM_BOT_TOKEN": "tg",
|
||||
"DEEPSEEK_API_KEY": "ds",
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "BRAVE_API_KEY is required",
|
||||
},
|
||||
{
|
||||
name: "defaults applied",
|
||||
env: map[string]string{
|
||||
"TELEGRAM_BOT_TOKEN": "tg",
|
||||
"DEEPSEEK_API_KEY": "ds",
|
||||
"BRAVE_API_KEY": "br",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "override defaults",
|
||||
env: map[string]string{
|
||||
"TELEGRAM_BOT_TOKEN": "tg",
|
||||
"DEEPSEEK_API_KEY": "ds",
|
||||
"BRAVE_API_KEY": "br",
|
||||
"POSTGRES_CONNECTION": "pg://custom",
|
||||
"DEEPSEEK_BASE_URL": "https://custom",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
os.Clearenv()
|
||||
for k, v := range tt.env {
|
||||
os.Setenv(k, v)
|
||||
}
|
||||
|
||||
cfg, err := Load()
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if tt.errMsg != "" && err.Error() != tt.errMsg {
|
||||
t.Errorf("expected error %q, got %q", tt.errMsg, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.TelegramBotToken != tt.env["TELEGRAM_BOT_TOKEN"] {
|
||||
t.Error("TelegramBotToken mismatch")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// User represents a Jules user.
|
||||
type User struct {
|
||||
ID string
|
||||
PrefferedChat string
|
||||
Language string
|
||||
}
|
||||
|
||||
// Chat links a user to an external messaging platform.
|
||||
type Chat struct {
|
||||
UserID string
|
||||
Platform string // "telegram", "email", "whatsapp"
|
||||
Identifier string // @username, email, phone
|
||||
}
|
||||
|
||||
// Metadata stores facts Jules knows about a user.
|
||||
type Metadata struct {
|
||||
UserID string
|
||||
CreatedAt time.Time
|
||||
Value string // "mom's name is Irina", "sleeps at 23:30"
|
||||
}
|
||||
|
||||
// Contact represents a relationship between two Jules users.
|
||||
type Contact struct {
|
||||
OwnerID string // User who owns this contact
|
||||
TargetID string // Target user ID
|
||||
Name string // "mom", "brother", "Lena"
|
||||
}
|
||||
|
||||
// Notification is a scheduled reminder or check-in.
|
||||
type Notification struct {
|
||||
ID string
|
||||
UserID string
|
||||
ScheduledAt time.Time
|
||||
Content string // "call mom", "morning workout"
|
||||
}
|
||||
|
||||
// Action records an interaction between Jules and a user.
|
||||
type Action struct {
|
||||
UserID string
|
||||
Type string // "user_msg", "jules_msg", "call", "ping_contact"
|
||||
Content string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// UserRepository manages User persistence.
|
||||
type UserRepository interface {
|
||||
Create(ctx context.Context) (*User, error)
|
||||
Get(ctx context.Context, id string) (*User, error)
|
||||
Delete(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
// ChatRepository manages Chat persistence.
|
||||
type ChatRepository interface {
|
||||
Attach(ctx context.Context, userID, platform, identifier string) error
|
||||
GetUserID(ctx context.Context, platform, identifier string) (string, error)
|
||||
GetChats(ctx context.Context, userID string) ([]Chat, error)
|
||||
Detach(ctx context.Context, userID, platform string) error
|
||||
}
|
||||
|
||||
// MetadataRepository manages Metadata persistence.
|
||||
type MetadataRepository interface {
|
||||
Add(ctx context.Context, userID, value string) error
|
||||
GetAll(ctx context.Context, userID string) ([]Metadata, error)
|
||||
Delete(ctx context.Context, userID, value string) error
|
||||
DeleteOlderThan(ctx context.Context, userID string, t time.Time) error
|
||||
}
|
||||
|
||||
// ContactRepository manages Contact persistence.
|
||||
type ContactRepository interface {
|
||||
Add(ctx context.Context, contact *Contact) error
|
||||
Get(ctx context.Context, ownerID, name string) (*Contact, error)
|
||||
GetAll(ctx context.Context, ownerID string) ([]Contact, error)
|
||||
Delete(ctx context.Context, ownerID, name string) error
|
||||
}
|
||||
|
||||
// NotificationRepository manages the notification queue.
|
||||
type NotificationRepository interface {
|
||||
Push(ctx context.Context, n *Notification) error
|
||||
Pop(ctx context.Context, limit int) ([]Notification, error)
|
||||
}
|
||||
|
||||
// ActionRepository manages the action log.
|
||||
type ActionRepository interface {
|
||||
Log(ctx context.Context, a *Action) error
|
||||
GetRecent(ctx context.Context, userID string, limit int) ([]Action, error)
|
||||
DeleteOlderThan(ctx context.Context, userID string, t time.Time) error
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package db
|
||||
@@ -0,0 +1 @@
|
||||
package db
|
||||
@@ -0,0 +1,28 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
environment:
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_DB: db
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- /tmp/pg:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U p_user -d nf"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
|
||||
telegram_api:
|
||||
image: ilya2ik/tgbotapiserver:latest
|
||||
ports:
|
||||
- "8081:8081"
|
||||
environment:
|
||||
SERVER_PORT: 8081
|
||||
SERVER_HOST: 0.0.0.0
|
||||
BOT_TOKEN: "test_token"
|
||||
LOG_LEVEL: "info"
|
||||
@@ -0,0 +1 @@
|
||||
package engine
|
||||
@@ -0,0 +1 @@
|
||||
package engine
|
||||
@@ -0,0 +1,14 @@
|
||||
module github.com/d1nch8g/jules
|
||||
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,87 @@
|
||||
package deepseek
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const BaseURL = "https://api.deepseek.com"
|
||||
|
||||
type Client struct {
|
||||
apiKey string
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func New(apiKey, baseURL string) *Client {
|
||||
return &Client{
|
||||
apiKey: apiKey,
|
||||
baseURL: baseURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: time.Minute,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type request struct {
|
||||
Model string `json:"model"`
|
||||
Messages []message `json:"messages"`
|
||||
}
|
||||
|
||||
type message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Choices []choice `json:"choices"`
|
||||
}
|
||||
|
||||
type choice struct {
|
||||
Message message `json:"message"`
|
||||
}
|
||||
|
||||
func (c *Client) Process(ctx context.Context, prompt string) (string, error) {
|
||||
reqBody := request{
|
||||
Model: "deepseek-chat",
|
||||
Messages: []message{{Role: "user", Content: prompt}},
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/chat/completions", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
errBody, _ := io.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("status %d: %s", resp.StatusCode, errBody)
|
||||
}
|
||||
|
||||
var result response
|
||||
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(result.Choices) == 0 {
|
||||
return "", errors.New("no choices in response")
|
||||
}
|
||||
|
||||
return result.Choices[0].Message.Content, nil
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package deepseek
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
client := New("key", "url")
|
||||
assert.Equal(t, "key", client.apiKey)
|
||||
assert.Equal(t, "url", client.baseURL)
|
||||
assert.NotNil(t, client.httpClient)
|
||||
}
|
||||
|
||||
func TestProcess_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/v1/chat/completions", r.URL.Path)
|
||||
assert.Equal(t, "Bearer key", r.Header.Get("Authorization"))
|
||||
|
||||
resp := response{Choices: []choice{{Message: message{Content: "response"}}}}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := New("key", server.URL)
|
||||
result, err := client.Process(context.Background(), "prompt")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "response", result)
|
||||
}
|
||||
|
||||
func TestProcess_HTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := New("key", server.URL)
|
||||
_, err := client.Process(context.Background(), "prompt")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "status 500")
|
||||
}
|
||||
|
||||
func TestProcess_EmptyChoices(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(response{Choices: []choice{}})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := New("key", server.URL)
|
||||
_, err := client.Process(context.Background(), "prompt")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no choices")
|
||||
}
|
||||
|
||||
func TestProcess_InvalidJSON(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("not json"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := New("key", server.URL)
|
||||
_, err := client.Process(context.Background(), "prompt")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestProcess_NetworkError(t *testing.T) {
|
||||
client := New("key", "http://localhost:9999")
|
||||
_, err := client.Process(context.Background(), "prompt")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestProcess_InvalidURL(t *testing.T) {
|
||||
client := New("key", string([]byte{0x7f}))
|
||||
_, err := client.Process(context.Background(), "prompt")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package llm
|
||||
|
||||
import "context"
|
||||
|
||||
type LLM interface {
|
||||
Process(ctx context.Context, prompt string) (string, error)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package brave
|
||||
@@ -0,0 +1 @@
|
||||
package brave
|
||||
@@ -0,0 +1,7 @@
|
||||
package search
|
||||
|
||||
import "context"
|
||||
|
||||
type Searcher interface {
|
||||
Search(ctx context.Context, query string) (string, error)
|
||||
}
|
||||
Reference in New Issue
Block a user