From 6bbe01317005876f31f377e28f33c471e25ac123 Mon Sep 17 00:00:00 2001 From: wonder Date: Tue, 5 May 2026 11:37:19 +0800 Subject: [PATCH] feat: add Gin HTTP API with SSE streaming chat --- go.mod | 29 +++++++++-- go.sum | 47 ++++++++++++++++++ internal/api/handler_chat.go | 93 ++++++++++++++++++++++++++++++++++ internal/api/handler_index.go | 27 ++++++++++ internal/api/handler_notes.go | 94 +++++++++++++++++++++++++++++++++++ internal/api/router.go | 42 ++++++++++++++++ internal/api/types.go | 19 +++++++ main.go | 36 ++++++++++++-- 8 files changed, 377 insertions(+), 10 deletions(-) create mode 100644 internal/api/handler_chat.go create mode 100644 internal/api/handler_index.go create mode 100644 internal/api/handler_notes.go create mode 100644 internal/api/router.go create mode 100644 internal/api/types.go diff --git a/go.mod b/go.mod index 67f5c13..f0f38f6 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module eino-test -go 1.23.6 +go 1.25.0 require ( github.com/bahlo/generic-list-go v0.2.0 // indirect @@ -16,24 +16,43 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/eino-contrib/jsonschema v1.0.3 // indirect github.com/evanphx/json-patch v0.5.2 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gin-contrib/cors v1.7.7 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/gin-gonic/gin v1.12.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/goph/emperror v0.17.2 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/meguminnnnnnnnn/go-openai v0.1.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/nikolalohinski/gonja v1.5.3 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yargevad/filepathx v1.0.0 // indirect - golang.org/x/arch v0.15.0 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + golang.org/x/arch v0.23.0 // indirect + golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/sys v0.33.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1b3020f..780063a 100644 --- a/go.sum +++ b/go.sum @@ -35,7 +35,25 @@ github.com/eino-contrib/jsonschema v1.0.3/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGj github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/gin-contrib/cors v1.7.7 h1:Oh9joP463x7Mw72vhvJ61YQm8ODh9b04YR7vsOErD0Q= +github.com/gin-contrib/cors v1.7.7/go.mod h1:K5tW0RkzJtWSiOdikXloy8VEZlgdVNpHNw8FpjUPNrE= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -50,12 +68,18 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/meguminnnnnnnnn/go-openai v0.1.2 h1:iXombGGjqjBrmE9WaSidUhhi3YQhf42QTHvHLMkgvCA= github.com/meguminnnnnnnnn/go-openai v0.1.2/go.mod h1:qs96ysDmxhE4BZoU45I43zcyfnaYxU3X+aRzLko/htY= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -70,10 +94,16 @@ github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -94,23 +124,40 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= +golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= +golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/internal/api/handler_chat.go b/internal/api/handler_chat.go new file mode 100644 index 0000000..6e94e37 --- /dev/null +++ b/internal/api/handler_chat.go @@ -0,0 +1,93 @@ +package api + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/schema" + "github.com/gin-gonic/gin" +) + +type ChatHandler struct { + supervisor adk.Agent +} + +func NewChatHandler(supervisor adk.Agent) *ChatHandler { + return &ChatHandler{supervisor: supervisor} +} + +func (h *ChatHandler) Chat(c *gin.Context) { + var req ChatRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.Header("Content-Type", "text/event-stream") + c.Header("Cache-Control", "no-cache") + c.Header("Connection", "keep-alive") + c.Header("X-Accel-Buffering", "no") + + iter := h.supervisor.Run(c.Request.Context(), &adk.AgentInput{ + Messages: []adk.Message{ + schema.UserMessage(req.Message), + }, + EnableStreaming: true, + }) + + c.Stream(func(w io.Writer) bool { + event, ok := iter.Next() + if !ok { + return false + } + + if event.Err != nil { + writeSSE(w, "error", map[string]string{"error": event.Err.Error()}) + return false + } + + if event.Output == nil || event.Output.MessageOutput == nil { + return true + } + + mv := event.Output.MessageOutput + + if mv.IsStreaming && mv.MessageStream != nil { + stream := mv.MessageStream + for { + msg, err := stream.Recv() + if err == io.EOF { + break + } + if err != nil { + break + } + if msg.Content != "" { + writeSSE(w, "message", map[string]string{ + "agent": event.AgentName, + "content": msg.Content, + "role": string(mv.Role), + }) + } + } + } else if mv.Message != nil { + if mv.Message.Content != "" { + writeSSE(w, "message", map[string]string{ + "agent": event.AgentName, + "content": mv.Message.Content, + "role": string(mv.Role), + }) + } + } + + return true + }) +} + +func writeSSE(w io.Writer, event string, data any) { + b, _ := json.Marshal(data) + fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event, string(b)) +} diff --git a/internal/api/handler_index.go b/internal/api/handler_index.go new file mode 100644 index 0000000..daffcee --- /dev/null +++ b/internal/api/handler_index.go @@ -0,0 +1,27 @@ +package api + +import ( + "net/http" + + "eino-test/config" + "eino-test/internal/rag" + + "github.com/gin-gonic/gin" +) + +type IndexHandler struct { + pipeline *rag.RAGPipeline + cfg *config.Config +} + +func NewIndexHandler(pipeline *rag.RAGPipeline, cfg *config.Config) *IndexHandler { + return &IndexHandler{pipeline: pipeline, cfg: cfg} +} + +func (h *IndexHandler) RebuildIndex(c *gin.Context) { + if err := h.pipeline.IndexAllFromDir(c.Request.Context(), h.cfg.DataDir); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "index rebuilt"}) +} diff --git a/internal/api/handler_notes.go b/internal/api/handler_notes.go new file mode 100644 index 0000000..a13f4be --- /dev/null +++ b/internal/api/handler_notes.go @@ -0,0 +1,94 @@ +package api + +import ( + "net/http" + "time" + + "eino-test/internal/rag" + "eino-test/internal/store" + + "github.com/gin-gonic/gin" +) + +type NoteHandler struct { + pipeline *rag.RAGPipeline +} + +func NewNoteHandler(pipeline *rag.RAGPipeline) *NoteHandler { + return &NoteHandler{pipeline: pipeline} +} + +func (h *NoteHandler) ListNotes(c *gin.Context) { + notes, err := h.pipeline.NoteStore().List(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + var resp []NoteResponse + for _, n := range notes { + resp = append(resp, NoteResponse{ + ID: n.ID, + Title: n.Title, + Tags: n.Tags, + CreatedAt: n.CreatedAt.Format(time.RFC3339), + }) + } + c.JSON(http.StatusOK, resp) +} + +func (h *NoteHandler) GetNote(c *gin.Context) { + id := c.Param("id") + note, err := h.pipeline.NoteStore().GetByID(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "note not found"}) + return + } + c.JSON(http.StatusOK, NoteResponse{ + ID: note.ID, + Title: note.Title, + Content: note.Content, + Tags: note.Tags, + CreatedAt: note.CreatedAt.Format(time.RFC3339), + }) +} + +func (h *NoteHandler) CreateNote(c *gin.Context) { + var req NoteRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + note := &store.Note{ + ID: req.Title, + Title: req.Title, + Content: req.Content, + Tags: req.Tags, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + if err := h.pipeline.NoteStore().Create(c.Request.Context(), note); err != nil { + c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) + return + } + if err := h.pipeline.IndexNote(c.Request.Context(), note); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusCreated, NoteResponse{ + ID: note.ID, + Title: note.Title, + Content: note.Content, + Tags: note.Tags, + CreatedAt: note.CreatedAt.Format(time.RFC3339), + }) +} + +func (h *NoteHandler) DeleteNote(c *gin.Context) { + id := c.Param("id") + if err := h.pipeline.NoteStore().Delete(c.Request.Context(), id); err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "note not found"}) + return + } + h.pipeline.RemoveNote(id) + c.JSON(http.StatusOK, gin.H{"message": "deleted"}) +} diff --git a/internal/api/router.go b/internal/api/router.go new file mode 100644 index 0000000..da1e470 --- /dev/null +++ b/internal/api/router.go @@ -0,0 +1,42 @@ +package api + +import ( + "eino-test/config" + "eino-test/internal/rag" + + "github.com/cloudwego/eino/adk" + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +func NewRouter(cfg *config.Config, pipeline *rag.RAGPipeline, supervisor adk.Agent) *gin.Engine { + r := gin.Default() + + r.Use(cors.New(cors.Config{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, + AllowCredentials: true, + })) + + chatHandler := NewChatHandler(supervisor) + noteHandler := NewNoteHandler(pipeline) + indexHandler := NewIndexHandler(pipeline, cfg) + + api := r.Group("/api") + { + api.POST("/chat", chatHandler.Chat) + api.GET("/notes", noteHandler.ListNotes) + api.GET("/notes/:id", noteHandler.GetNote) + api.POST("/notes", noteHandler.CreateNote) + api.DELETE("/notes/:id", noteHandler.DeleteNote) + api.POST("/index", indexHandler.RebuildIndex) + } + + r.Static("/static", "./web/dist") + r.NoRoute(func(c *gin.Context) { + c.File("./web/dist/index.html") + }) + + return r +} diff --git a/internal/api/types.go b/internal/api/types.go new file mode 100644 index 0000000..89d8637 --- /dev/null +++ b/internal/api/types.go @@ -0,0 +1,19 @@ +package api + +type ChatRequest struct { + Message string `json:"message" binding:"required"` +} + +type NoteRequest struct { + Title string `json:"title" binding:"required"` + Content string `json:"content" binding:"required"` + Tags []string `json:"tags"` +} + +type NoteResponse struct { + ID string `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + Tags []string `json:"tags"` + CreatedAt string `json:"created_at"` +} diff --git a/main.go b/main.go index ea57497..85d8061 100644 --- a/main.go +++ b/main.go @@ -1,18 +1,44 @@ package main import ( - "fmt" + "context" "log" "eino-test/config" + "eino-test/internal/agent" + "eino-test/internal/api" + "eino-test/internal/embedding" + "eino-test/internal/index" + "eino-test/internal/rag" "eino-test/internal/store" ) func main() { cfg := config.Load() - noteStore := store.NewMemoryNoteStore() - _ = noteStore + ctx := context.Background() - log.Printf("Server starting on port %s (data dir: %s)", cfg.Port, cfg.DataDir) - fmt.Println("Knowledge Assistant ready") + noteStore := store.NewMemoryNoteStore() + embedder, err := embedding.NewEmbedder(ctx, cfg.OpenAIAPIKey, cfg.OpenAIBaseURL, cfg.OpenAIEmbedModel) + if err != nil { + log.Fatalf("Failed to create embedder: %v", err) + } + + vectorIdx := index.NewInMemoryVectorIndex() + pipeline := rag.NewRAGPipeline(noteStore, embedder, vectorIdx) + + if err := pipeline.IndexAllFromDir(ctx, cfg.DataDir); err != nil { + log.Printf("Warning: failed to index notes: %v", err) + } + + supervisorAgent, err := agent.NewSupervisorAgent(ctx, cfg, pipeline) + if err != nil { + log.Fatalf("Failed to create supervisor agent: %v", err) + } + + router := api.NewRouter(cfg, pipeline, supervisorAgent) + + log.Printf("Server starting on port %s", cfg.Port) + if err := router.Run(":" + cfg.Port); err != nil { + log.Fatalf("Failed to start server: %v", err) + } }