Initial commit: GovAI 政务AI平台
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
package dify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewClient(baseURL string) *Client {
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 120 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) do(ctx context.Context, method, path, apiKey string, body any) (*http.Response, error) {
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal request: %w", err)
|
||||
}
|
||||
bodyReader = bytes.NewReader(data)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bodyReader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("execute request: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
defer resp.Body.Close()
|
||||
var apiErr APIError
|
||||
if err := json.NewDecoder(resp.Body).Decode(&apiErr); err != nil {
|
||||
return nil, fmt.Errorf("dify API error (status %d)", resp.StatusCode)
|
||||
}
|
||||
apiErr.Status = resp.StatusCode
|
||||
return nil, &apiErr
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) doJSON(ctx context.Context, method, path, apiKey string, body any, result any) error {
|
||||
resp, err := c.do(ctx, method, path, apiKey, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if result != nil {
|
||||
return json.NewDecoder(resp.Body).Decode(result)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package dify
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ChatStream sends a chat message and returns a reader for SSE events.
|
||||
// Caller is responsible for closing the returned io.ReadCloser.
|
||||
func (c *Client) ChatStream(ctx context.Context, apiKey string, req *ChatRequest) (io.ReadCloser, error) {
|
||||
req.ResponseMode = "streaming"
|
||||
|
||||
resp, err := c.do(ctx, "POST", "/chat-messages", apiKey, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
// ChatBlocking sends a chat message and waits for the complete response.
|
||||
func (c *Client) ChatBlocking(ctx context.Context, apiKey string, req *ChatRequest) (*ChatStreamEvent, error) {
|
||||
req.ResponseMode = "blocking"
|
||||
|
||||
var result ChatStreamEvent
|
||||
if err := c.doJSON(ctx, "POST", "/chat-messages", apiKey, req, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ParseSSEStream parses a Dify SSE stream and calls handler for each event.
|
||||
func ParseSSEStream(reader io.Reader, handler func(event ChatStreamEvent) error) error {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
scanner.Buffer(make([]byte, 64*1024), 256*1024)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
if !strings.HasPrefix(line, "data: ") {
|
||||
continue
|
||||
}
|
||||
|
||||
data := strings.TrimPrefix(line, "data: ")
|
||||
if data == "[DONE]" {
|
||||
break
|
||||
}
|
||||
|
||||
var event ChatStreamEvent
|
||||
if err := json.Unmarshal([]byte(data), &event); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := handler(event); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
// ListConversations returns the user's conversation list for an app.
|
||||
func (c *Client) ListConversations(ctx context.Context, apiKey, user string, limit int, firstID string) (*ConversationListResponse, error) {
|
||||
path := fmt.Sprintf("/conversations?user=%s&limit=%d", user, limit)
|
||||
if firstID != "" {
|
||||
path += "&first_id=" + firstID
|
||||
}
|
||||
|
||||
var result ConversationListResponse
|
||||
if err := c.doJSON(ctx, "GET", path, apiKey, nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ListMessages returns messages in a conversation.
|
||||
func (c *Client) ListMessages(ctx context.Context, apiKey, user, conversationID string, limit int, firstID string) (*MessageListResponse, error) {
|
||||
path := fmt.Sprintf("/messages?user=%s&conversation_id=%s&limit=%d", user, conversationID, limit)
|
||||
if firstID != "" {
|
||||
path += "&first_id=" + firstID
|
||||
}
|
||||
|
||||
var result MessageListResponse
|
||||
if err := c.doJSON(ctx, "GET", path, apiKey, nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// DeleteConversation deletes a conversation.
|
||||
func (c *Client) DeleteConversation(ctx context.Context, apiKey, user, conversationID string) error {
|
||||
body := map[string]string{"user": user}
|
||||
return c.doJSON(ctx, "DELETE", "/conversations/"+conversationID, apiKey, body, nil)
|
||||
}
|
||||
|
||||
// SubmitFeedback submits feedback for a message.
|
||||
func (c *Client) SubmitFeedback(ctx context.Context, apiKey, messageID string, req *FeedbackRequest) error {
|
||||
return c.doJSON(ctx, "POST", "/messages/"+messageID+"/feedbacks", apiKey, req, nil)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package dify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// CreateDataset creates a new knowledge base (dataset) in Dify.
|
||||
func (c *Client) CreateDataset(ctx context.Context, apiKey string, req *DatasetCreateRequest) (*Dataset, error) {
|
||||
var result Dataset
|
||||
if err := c.doJSON(ctx, "POST", "/datasets", apiKey, req, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// DeleteDataset deletes a knowledge base.
|
||||
func (c *Client) DeleteDataset(ctx context.Context, apiKey, datasetID string) error {
|
||||
return c.doJSON(ctx, "DELETE", "/datasets/"+datasetID, apiKey, nil, nil)
|
||||
}
|
||||
|
||||
// UploadDocument uploads a file to a dataset for indexing.
|
||||
func (c *Client) UploadDocument(ctx context.Context, apiKey, datasetID string, filename string, fileReader io.Reader) (*DocumentIndexingStatus, error) {
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
|
||||
part, err := writer.CreateFormFile("file", filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create form file: %w", err)
|
||||
}
|
||||
if _, err := io.Copy(part, fileReader); err != nil {
|
||||
return nil, fmt.Errorf("copy file: %w", err)
|
||||
}
|
||||
|
||||
// indexing mode
|
||||
if err := writer.WriteField("indexing_technique", "high_quality"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := writer.WriteField("process_rule", `{"mode": "automatic"}`); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
writer.Close()
|
||||
|
||||
path := fmt.Sprintf("/datasets/%s/document/create_by_file", datasetID)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+path, &buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("upload failed (status %d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// Dify returns document info in response
|
||||
var result struct {
|
||||
Document DocumentIndexingStatus `json:"document"`
|
||||
}
|
||||
if err := decodeJSON(resp.Body, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result.Document, nil
|
||||
}
|
||||
|
||||
// DeleteDocument deletes a document from a dataset.
|
||||
func (c *Client) DeleteDocument(ctx context.Context, apiKey, datasetID, documentID string) error {
|
||||
path := fmt.Sprintf("/datasets/%s/documents/%s", datasetID, documentID)
|
||||
return c.doJSON(ctx, "DELETE", path, apiKey, nil, nil)
|
||||
}
|
||||
|
||||
// GetDocumentIndexingStatus checks the indexing status of a document.
|
||||
func (c *Client) GetDocumentIndexingStatus(ctx context.Context, apiKey, datasetID, batch string) (*DocumentIndexingStatus, error) {
|
||||
path := fmt.Sprintf("/datasets/%s/documents/%s/indexing-status", datasetID, batch)
|
||||
var result DocumentIndexingStatus
|
||||
if err := c.doJSON(ctx, "GET", path, apiKey, nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func decodeJSON(r io.Reader, v any) error {
|
||||
return json.NewDecoder(r).Decode(v)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package dify
|
||||
|
||||
import "time"
|
||||
|
||||
// --- Request types ---
|
||||
|
||||
type ChatRequest struct {
|
||||
Query string `json:"query"`
|
||||
Inputs map[string]any `json:"inputs,omitempty"`
|
||||
ConversationID string `json:"conversation_id,omitempty"`
|
||||
User string `json:"user"`
|
||||
ResponseMode string `json:"response_mode"`
|
||||
}
|
||||
|
||||
type CompletionRequest struct {
|
||||
Inputs map[string]any `json:"inputs"`
|
||||
User string `json:"user"`
|
||||
ResponseMode string `json:"response_mode"`
|
||||
}
|
||||
|
||||
type FeedbackRequest struct {
|
||||
Rating string `json:"rating"` // "like", "dislike", null
|
||||
User string `json:"user"`
|
||||
}
|
||||
|
||||
// --- Response types ---
|
||||
|
||||
type ChatStreamEvent struct {
|
||||
Event string `json:"event"`
|
||||
TaskID string `json:"task_id"`
|
||||
MessageID string `json:"message_id"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
Answer string `json:"answer"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
type MessageEndMetadata struct {
|
||||
Usage TokenUsage `json:"usage"`
|
||||
}
|
||||
|
||||
type TokenUsage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
}
|
||||
|
||||
type Conversation struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Inputs any `json:"inputs"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ConversationListResponse struct {
|
||||
Data []Conversation `json:"data"`
|
||||
HasMore bool `json:"has_more"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
ID string `json:"id"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
Query string `json:"query"`
|
||||
Answer string `json:"answer"`
|
||||
Feedback *Feedback `json:"feedback"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Inputs map[string]any `json:"inputs"`
|
||||
}
|
||||
|
||||
type Feedback struct {
|
||||
Rating string `json:"rating"`
|
||||
}
|
||||
|
||||
type MessageListResponse struct {
|
||||
Data []Message `json:"data"`
|
||||
HasMore bool `json:"has_more"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// --- Dataset/Knowledge types ---
|
||||
|
||||
type DatasetCreateRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
type Dataset struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
DocCount int `json:"document_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type DocumentIndexingStatus struct {
|
||||
ID string `json:"id"`
|
||||
IndexingStatus string `json:"indexing_status"`
|
||||
ProcessingStart string `json:"processing_started_at"`
|
||||
CompletedAt string `json:"completed_at"`
|
||||
}
|
||||
|
||||
// --- Error ---
|
||||
|
||||
type APIError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
Reference in New Issue
Block a user