Skip to content

Commit c4aa153

Browse files
warjiangkasanatte
authored andcommitted
feat(mcp): integrate MCP server
Co-authored-by: warjiang <[email protected]> Signed-off-by: kasanatte <[email protected]>
1 parent 223377c commit c4aa153

File tree

13 files changed

+1886
-0
lines changed

13 files changed

+1886
-0
lines changed

cmd/api/app/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828

2929
"github.com/karmada-io/dashboard/cmd/api/app/options"
3030
"github.com/karmada-io/dashboard/cmd/api/app/router"
31+
_ "github.com/karmada-io/dashboard/cmd/api/app/routes/assistant" // Importing route packages forces route registration
3132
_ "github.com/karmada-io/dashboard/cmd/api/app/routes/auth" // Importing route packages forces route registration
3233
_ "github.com/karmada-io/dashboard/cmd/api/app/routes/cluster" // Importing route packages forces route registration
3334
_ "github.com/karmada-io/dashboard/cmd/api/app/routes/clusteroverridepolicy" // Importing route packages forces route registration
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
Copyright 2024 The Karmada Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package assistant
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"errors"
23+
"fmt"
24+
"io"
25+
"net/http"
26+
"os"
27+
"strings"
28+
29+
"github.com/gin-gonic/gin"
30+
"github.com/sashabaranov/go-openai"
31+
"k8s.io/klog/v2"
32+
33+
"github.com/karmada-io/dashboard/cmd/api/app/router"
34+
)
35+
36+
func init() {
37+
// Register routes
38+
router.V1().POST("/assistant", Answering)
39+
router.V1().POST("/chat", ChatHandler)
40+
router.V1().GET("/chat/tools", GetMCPToolsHandler)
41+
}
42+
43+
// AnsweringRequest represents the request payload for the legacy assistant endpoint.
44+
type AnsweringRequest struct {
45+
Prompt string `json:"prompt"`
46+
Message string `json:"message"`
47+
}
48+
49+
// StreamResponse is used for the legacy SSE stream response.
50+
type StreamResponse struct {
51+
Type string `json:"type"`
52+
Content interface{} `json:"content"`
53+
}
54+
55+
// getOpenAIModel returns the appropriate model based on environment configuration.
56+
func getOpenAIModel() string {
57+
if model := os.Getenv("OPENAI_MODEL"); model != "" {
58+
return model
59+
}
60+
return openai.GPT3Dot5Turbo // Default fallback
61+
}
62+
63+
// Answering is a handler for the legacy, non-MCP chat endpoint.
64+
func Answering(c *gin.Context) {
65+
session, err := newAnsweringSession(c)
66+
if err != nil {
67+
klog.Errorf("Failed to create answering session: %v", err)
68+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
69+
return
70+
}
71+
72+
if err := session.run(); err != nil {
73+
klog.Errorf("Answering session run failed: %v", err)
74+
}
75+
}
76+
77+
// answeringSession manages the state for a legacy chat request.
78+
type answeringSession struct {
79+
ctx context.Context
80+
writer http.ResponseWriter
81+
flusher http.Flusher
82+
userInput string
83+
openAIClient *openai.Client
84+
}
85+
86+
// newAnsweringSession creates a new session for the legacy Answering handler.
87+
func newAnsweringSession(c *gin.Context) (*answeringSession, error) {
88+
var request AnsweringRequest
89+
if err := c.ShouldBindJSON(&request); err != nil {
90+
return nil, fmt.Errorf("invalid request body: %w", err)
91+
}
92+
93+
userInput := strings.TrimSpace(request.Prompt)
94+
if userInput == "" {
95+
userInput = strings.TrimSpace(request.Message)
96+
}
97+
if userInput == "" {
98+
return nil, errors.New("prompt cannot be empty")
99+
}
100+
101+
flusher, ok := c.Writer.(http.Flusher)
102+
if !ok {
103+
return nil, errors.New("streaming unsupported")
104+
}
105+
106+
client, err := prepareOpenAIClient()
107+
if err != nil {
108+
return nil, err
109+
}
110+
111+
return &answeringSession{
112+
ctx: c.Request.Context(),
113+
writer: c.Writer,
114+
flusher: flusher,
115+
userInput: userInput,
116+
openAIClient: client,
117+
}, nil
118+
}
119+
120+
// run executes the chat flow for the legacy Answering handler.
121+
func (s *answeringSession) run() error {
122+
setupSSEHeaders(s.writer)
123+
124+
systemMessage := "You are a helpful assistant for Karmada cluster management." +
125+
"You can provide guidance about Karmada concepts, best practices, and configuration help."
126+
127+
messages := []openai.ChatCompletionMessage{
128+
{Role: openai.ChatMessageRoleSystem, Content: systemMessage},
129+
{Role: openai.ChatMessageRoleUser, Content: s.userInput},
130+
}
131+
132+
req := openai.ChatCompletionRequest{
133+
Model: getOpenAIModel(),
134+
Messages: messages,
135+
Stream: true,
136+
}
137+
138+
stream, err := s.openAIClient.CreateChatCompletionStream(s.ctx, req)
139+
if err != nil {
140+
return fmt.Errorf("could not create chat completion stream: %w", err)
141+
}
142+
defer stream.Close()
143+
144+
for {
145+
response, err := stream.Recv()
146+
if errors.Is(err, io.EOF) {
147+
break
148+
}
149+
if err != nil {
150+
return fmt.Errorf("stream reception error: %w", err)
151+
}
152+
153+
if len(response.Choices) > 0 && response.Choices[0].Delta.Content != "" {
154+
s.sendStreamEvent("text", response.Choices[0].Delta.Content)
155+
}
156+
}
157+
158+
s.sendStreamEvent("completion", nil)
159+
return nil
160+
}
161+
162+
// sendStreamEvent marshals and sends a StreamResponse to the client.
163+
func (s *answeringSession) sendStreamEvent(eventType string, content interface{}) {
164+
msg := StreamResponse{Type: eventType, Content: content}
165+
data, err := json.Marshal(msg)
166+
if err != nil {
167+
klog.Errorf("Failed to marshal stream event: %v", err)
168+
return
169+
}
170+
fmt.Fprintf(s.writer, "data: %s\n\n", data)
171+
s.flusher.Flush()
172+
}

0 commit comments

Comments
 (0)