mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-20 01:34:24 +08:00
code refactor
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
package ollama
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/llm/data"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/llm/provider"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/llm/provider/common"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
)
|
||||
|
||||
const ollamaChatCompletionsPath = "api/chat"
|
||||
|
||||
// OllamaLargeLanguageModelAdapter defines the structure of Ollama large language model adapter
|
||||
type OllamaLargeLanguageModelAdapter struct {
|
||||
common.HttpLargeLanguageModelAdapter
|
||||
OllamaServerURL string
|
||||
OllamaModelID string
|
||||
}
|
||||
|
||||
// OllamaMessageRole defines the role of Ollama chat message
|
||||
type OllamaMessageRole string
|
||||
|
||||
const (
|
||||
OllamaMessageRoleSystem OllamaMessageRole = "system"
|
||||
OllamaMessageRoleUser OllamaMessageRole = "user"
|
||||
)
|
||||
|
||||
// OllamaChatRequest defines the structure of Ollama chat request
|
||||
type OllamaChatRequest struct {
|
||||
Model string `json:"model"`
|
||||
Stream bool `json:"stream"`
|
||||
Messages []*OllamaChatRequestMessage `json:"messages"`
|
||||
Format string `json:"format,omitempty"`
|
||||
}
|
||||
|
||||
// OllamaChatRequestMessage defines the structure of Ollama chat request message
|
||||
type OllamaChatRequestMessage struct {
|
||||
Role OllamaMessageRole `json:"role"`
|
||||
Content string `json:"content"`
|
||||
Images []string `json:"images,omitempty"`
|
||||
}
|
||||
|
||||
// OllamaChatResponse defines the structure of Ollama chat response
|
||||
type OllamaChatResponse struct {
|
||||
Message *OllamaChatResponseMessage `json:"message"`
|
||||
}
|
||||
|
||||
// OllamaChatResponseMessage defines the structure of Ollama chat response message
|
||||
type OllamaChatResponseMessage struct {
|
||||
Content *string `json:"content"`
|
||||
}
|
||||
|
||||
// BuildTextualRequest returns the http request by Ollama large language model adapter
|
||||
func (p *OllamaLargeLanguageModelAdapter) BuildTextualRequest(c core.Context, uid int64, request *data.LargeLanguageModelRequest, responseType data.LargeLanguageModelResponseFormat) (*http.Request, error) {
|
||||
requestBody, err := p.buildJsonRequestBody(c, uid, request, responseType)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpRequest, err := http.NewRequest("POST", p.getOllamaRequestUrl(), bytes.NewReader(requestBody))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
|
||||
return httpRequest, nil
|
||||
}
|
||||
|
||||
// ParseTextualResponse returns the textual response by Ollama large language model adapter
|
||||
func (p *OllamaLargeLanguageModelAdapter) ParseTextualResponse(c core.Context, uid int64, body []byte, responseType data.LargeLanguageModelResponseFormat) (*data.LargeLanguageModelTextualResponse, error) {
|
||||
chatResponse := &OllamaChatResponse{}
|
||||
err := json.Unmarshal(body, &chatResponse)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[ollama_large_language_model_adapter.ParseTextualResponse] failed to parse chat response for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrFailedToRequestRemoteApi
|
||||
}
|
||||
|
||||
if chatResponse == nil || chatResponse.Message == nil || chatResponse.Message.Content == nil {
|
||||
log.Errorf(c, "[ollama_large_language_model_adapter.ParseTextualResponse] chat response is invalid for user \"uid:%d\"", uid)
|
||||
return nil, errs.ErrFailedToRequestRemoteApi
|
||||
}
|
||||
|
||||
textualResponse := &data.LargeLanguageModelTextualResponse{
|
||||
Content: *chatResponse.Message.Content,
|
||||
}
|
||||
|
||||
return textualResponse, nil
|
||||
}
|
||||
|
||||
func (p *OllamaLargeLanguageModelAdapter) buildJsonRequestBody(c core.Context, uid int64, request *data.LargeLanguageModelRequest, responseType data.LargeLanguageModelResponseFormat) ([]byte, error) {
|
||||
if p.OllamaModelID == "" {
|
||||
return nil, errs.ErrInvalidLLMModelId
|
||||
}
|
||||
|
||||
chatRequest := &OllamaChatRequest{
|
||||
Model: p.OllamaModelID,
|
||||
Stream: request.Stream,
|
||||
Messages: make([]*OllamaChatRequestMessage, 0, 2),
|
||||
}
|
||||
|
||||
if request.SystemPrompt != "" {
|
||||
chatRequest.Messages = append(chatRequest.Messages, &OllamaChatRequestMessage{
|
||||
Role: OllamaMessageRoleSystem,
|
||||
Content: request.SystemPrompt,
|
||||
})
|
||||
}
|
||||
|
||||
if len(request.UserPrompt) > 0 {
|
||||
if request.UserPromptType == data.LARGE_LANGUAGE_MODEL_REQUEST_PROMPT_TYPE_IMAGE_URL {
|
||||
imageBase64Data := base64.StdEncoding.EncodeToString(request.UserPrompt)
|
||||
chatRequest.Messages = append(chatRequest.Messages, &OllamaChatRequestMessage{
|
||||
Role: OllamaMessageRoleUser,
|
||||
Images: []string{imageBase64Data},
|
||||
})
|
||||
} else {
|
||||
chatRequest.Messages = append(chatRequest.Messages, &OllamaChatRequestMessage{
|
||||
Role: OllamaMessageRoleUser,
|
||||
Content: string(request.UserPrompt),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if responseType == data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON {
|
||||
chatRequest.Format = "json"
|
||||
}
|
||||
|
||||
requestBodyBytes, err := json.Marshal(chatRequest)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[ollama_large_language_model_adapter.buildJsonRequestBody] failed to marshal request body for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
log.Debugf(c, "[ollama_large_language_model_adapter.buildJsonRequestBody] request body is %s", requestBodyBytes)
|
||||
return requestBodyBytes, nil
|
||||
}
|
||||
|
||||
func (p *OllamaLargeLanguageModelAdapter) getOllamaRequestUrl() string {
|
||||
url := p.OllamaServerURL
|
||||
|
||||
if url[len(url)-1] != '/' {
|
||||
url += "/"
|
||||
}
|
||||
|
||||
url += ollamaChatCompletionsPath
|
||||
return url
|
||||
}
|
||||
|
||||
// NewOllamaLargeLanguageModelProvider creates a new Ollama large language model provider instance
|
||||
func NewOllamaLargeLanguageModelProvider(llmConfig *settings.LLMConfig) provider.LargeLanguageModelProvider {
|
||||
return common.NewCommonHttpLargeLanguageModelProvider(&OllamaLargeLanguageModelAdapter{
|
||||
OllamaServerURL: llmConfig.OllamaServerURL,
|
||||
OllamaModelID: llmConfig.OllamaModelID,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package ollama
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/llm/data"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
)
|
||||
|
||||
func TestOllamaLargeLanguageModelAdapter_buildJsonRequestBody_TextualUserPrompt(t *testing.T) {
|
||||
adapter := &OllamaLargeLanguageModelAdapter{
|
||||
OllamaModelID: "test",
|
||||
}
|
||||
|
||||
request := &data.LargeLanguageModelRequest{
|
||||
SystemPrompt: "You are a helpful assistant.",
|
||||
UserPrompt: []byte("Hello, how are you?"),
|
||||
}
|
||||
|
||||
bodyBytes, err := adapter.buildJsonRequestBody(core.NewNullContext(), 0, request, data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var body map[string]interface{}
|
||||
err = json.Unmarshal(bodyBytes, &body)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, "{\"model\":\"test\",\"stream\":false,\"messages\":[{\"role\":\"system\",\"content\":\"You are a helpful assistant.\"},{\"role\":\"user\",\"content\":\"Hello, how are you?\"}],\"format\":\"json\"}", string(bodyBytes))
|
||||
}
|
||||
|
||||
func TestOllamaLargeLanguageModelAdapter_buildJsonRequestBody_ImageUserPrompt(t *testing.T) {
|
||||
adapter := &OllamaLargeLanguageModelAdapter{
|
||||
OllamaModelID: "test",
|
||||
}
|
||||
|
||||
request := &data.LargeLanguageModelRequest{
|
||||
SystemPrompt: "What's in this image?",
|
||||
UserPrompt: []byte("fakedata"),
|
||||
UserPromptType: data.LARGE_LANGUAGE_MODEL_REQUEST_PROMPT_TYPE_IMAGE_URL,
|
||||
}
|
||||
|
||||
bodyBytes, err := adapter.buildJsonRequestBody(core.NewNullContext(), 0, request, data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var body map[string]interface{}
|
||||
err = json.Unmarshal(bodyBytes, &body)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, "{\"model\":\"test\",\"stream\":false,\"messages\":[{\"role\":\"system\",\"content\":\"What's in this image?\"},{\"role\":\"user\",\"content\":\"\",\"images\":[\"ZmFrZWRhdGE=\"]}],\"format\":\"json\"}", string(bodyBytes))
|
||||
}
|
||||
|
||||
func TestOllamaLargeLanguageModelAdapter_ParseTextualResponse_ValidJsonResponse(t *testing.T) {
|
||||
adapter := &OllamaLargeLanguageModelAdapter{}
|
||||
|
||||
response := `{
|
||||
"model": "test",
|
||||
"created_at": "2025-09-01T01:02:03.456789Z",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "This is a test response"
|
||||
}
|
||||
}`
|
||||
|
||||
result, err := adapter.ParseTextualResponse(core.NewNullContext(), 0, []byte(response), data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "This is a test response", result.Content)
|
||||
}
|
||||
|
||||
func TestOllamaLargeLanguageModelAdapter_ParseTextualResponse_EmptyResponse(t *testing.T) {
|
||||
adapter := &OllamaLargeLanguageModelAdapter{}
|
||||
|
||||
response := `{
|
||||
"model": "test",
|
||||
"created_at": "2025-09-01T01:02:03.456789Z",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": ""
|
||||
}
|
||||
}`
|
||||
|
||||
result, err := adapter.ParseTextualResponse(core.NewNullContext(), 0, []byte(response), data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "", result.Content)
|
||||
}
|
||||
|
||||
func TestOllamaLargeLanguageModelAdapter_ParseTextualResponse_EmptyMessage(t *testing.T) {
|
||||
adapter := &OllamaLargeLanguageModelAdapter{}
|
||||
|
||||
response := `{
|
||||
"model": "test",
|
||||
"created_at": "2025-09-01T01:02:03.456789Z",
|
||||
"message": {}
|
||||
}`
|
||||
|
||||
_, err := adapter.ParseTextualResponse(core.NewNullContext(), 0, []byte(response), data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
|
||||
assert.EqualError(t, err, "failed to request third party api")
|
||||
}
|
||||
|
||||
func TestOllamaLargeLanguageModelAdapter_ParseTextualResponse_NoContentFieldInMessage(t *testing.T) {
|
||||
adapter := &OllamaLargeLanguageModelAdapter{}
|
||||
|
||||
response := `{
|
||||
"model": "test",
|
||||
"created_at": "2025-09-01T01:02:03.456789Z",
|
||||
"message": {
|
||||
"role": "assistant"
|
||||
}
|
||||
}`
|
||||
|
||||
_, err := adapter.ParseTextualResponse(core.NewNullContext(), 0, []byte(response), data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
|
||||
assert.EqualError(t, err, "failed to request third party api")
|
||||
}
|
||||
|
||||
func TestOllamaLargeLanguageModelAdapter_ParseTextualResponse_InvalidJson(t *testing.T) {
|
||||
adapter := &OllamaLargeLanguageModelAdapter{}
|
||||
|
||||
response := "error"
|
||||
|
||||
_, err := adapter.ParseTextualResponse(core.NewNullContext(), 0, []byte(response), data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
|
||||
assert.EqualError(t, err, "failed to request third party api")
|
||||
}
|
||||
|
||||
func TestOllamaLargeLanguageModelAdapter_GetOllamaRequestUrl(t *testing.T) {
|
||||
adapter := &OllamaLargeLanguageModelAdapter{
|
||||
OllamaServerURL: "http://localhost:11434/",
|
||||
}
|
||||
url := adapter.getOllamaRequestUrl()
|
||||
assert.Equal(t, "http://localhost:11434/api/chat", url)
|
||||
|
||||
adapter = &OllamaLargeLanguageModelAdapter{
|
||||
OllamaServerURL: "http://localhost:11434",
|
||||
}
|
||||
url = adapter.getOllamaRequestUrl()
|
||||
assert.Equal(t, "http://localhost:11434/api/chat", url)
|
||||
|
||||
adapter = &OllamaLargeLanguageModelAdapter{
|
||||
OllamaServerURL: "http://example.com/ollama/",
|
||||
}
|
||||
url = adapter.getOllamaRequestUrl()
|
||||
assert.Equal(t, "http://example.com/ollama/api/chat", url)
|
||||
}
|
||||
Reference in New Issue
Block a user