Skip to content

Commit 68e131c

Browse files
committed
feat[Go]: implement Langfuse API key management endpoints
Ports three Langfuse credential endpoints from Python (langfuse_api.py) to Go, closing issue #15675: POST/PUT /api/v1/langfuse/api-key (LangfuseHandler.SetAPIKey) GET /api/v1/langfuse/api-key (LangfuseHandler.GetAPIKey) DELETE /api/v1/langfuse/api-key (LangfuseHandler.DeleteAPIKey) New files: internal/entity/langfuse.go – TenantLangfuse entity (tenant_langfuse table) internal/dao/langfuse.go – LangfuseDAO: GetByTenantID, Create, UpdateByTenantID, DeleteByTenantID internal/service/langfuse.go – LangfuseService: SetAPIKey, GetAPIKey, DeleteAPIKey; langfuseAuthCheck and langfuseGetProjects HTTP helpers internal/handler/langfuse.go – LangfuseHandler with three HTTP handlers Changed files: internal/router/router.go – Router struct gains langfuseHandler; NewRouter constructs it; four routes added Functional parity with Python: SetAPIKey – validates all three fields; calls GET {host}/api/public/projects with Basic Auth (mirrors langfuse.auth_check()); upserts record; secret_key not echoed in response GetAPIKey – validates stored keys; fetches project list from Langfuse; returns public_key, host, project_id, project_name; secret_key intentionally excluded from response DeleteAPIKey – returns "Have not record" message when no keys stored, otherwise hard-deletes and returns true
1 parent 461c190 commit 68e131c

5 files changed

Lines changed: 493 additions & 39 deletions

File tree

internal/dao/langfuse.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//
2+
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
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 dao
18+
19+
import (
20+
"ragflow/internal/entity"
21+
)
22+
23+
// LangfuseDAO data access for tenant_langfuse table.
24+
type LangfuseDAO struct{}
25+
26+
// NewLangfuseDAO creates a LangfuseDAO.
27+
func NewLangfuseDAO() *LangfuseDAO {
28+
return &LangfuseDAO{}
29+
}
30+
31+
// GetByTenantID returns the Langfuse credential record for a tenant, or nil
32+
// when none exists.
33+
func (d *LangfuseDAO) GetByTenantID(tenantID string) (*entity.TenantLangfuse, error) {
34+
var entry entity.TenantLangfuse
35+
err := DB.Where("tenant_id = ?", tenantID).First(&entry).Error
36+
if err != nil {
37+
return nil, err
38+
}
39+
return &entry, nil
40+
}
41+
42+
// Create inserts a new TenantLangfuse record.
43+
func (d *LangfuseDAO) Create(entry *entity.TenantLangfuse) error {
44+
return DB.Create(entry).Error
45+
}
46+
47+
// UpdateByTenantID applies updates to the record for a tenant.
48+
func (d *LangfuseDAO) UpdateByTenantID(tenantID string, updates map[string]interface{}) error {
49+
return DB.Model(&entity.TenantLangfuse{}).
50+
Where("tenant_id = ?", tenantID).
51+
Updates(updates).Error
52+
}
53+
54+
// DeleteByTenantID hard-deletes the record for a tenant.
55+
func (d *LangfuseDAO) DeleteByTenantID(tenantID string) error {
56+
return DB.Unscoped().
57+
Where("tenant_id = ?", tenantID).
58+
Delete(&entity.TenantLangfuse{}).Error
59+
}

internal/entity/langfuse.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
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 entity
18+
19+
// TenantLangfuse stores per-tenant Langfuse credentials.
20+
type TenantLangfuse struct {
21+
ID string `gorm:"column:id;primaryKey;size:32" json:"id"`
22+
TenantID string `gorm:"column:tenant_id;size:32;not null;uniqueIndex" json:"tenant_id"`
23+
SecretKey string `gorm:"column:secret_key;size:255;not null" json:"secret_key"`
24+
PublicKey string `gorm:"column:public_key;size:255;not null" json:"public_key"`
25+
Host string `gorm:"column:host;size:255;not null" json:"host"`
26+
BaseModel
27+
}
28+
29+
// TableName maps to the tenant_langfuse table (matches Python model).
30+
func (TenantLangfuse) TableName() string {
31+
return "tenant_langfuse"
32+
}

internal/handler/langfuse.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
//
2+
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
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 handler
18+
19+
import (
20+
"net/http"
21+
22+
"github.com/gin-gonic/gin"
23+
24+
"ragflow/internal/common"
25+
"ragflow/internal/service"
26+
)
27+
28+
// LangfuseHandler manages Langfuse credential endpoints.
29+
type LangfuseHandler struct {
30+
langfuseService *service.LangfuseService
31+
}
32+
33+
// NewLangfuseHandler creates a LangfuseHandler.
34+
func NewLangfuseHandler() *LangfuseHandler {
35+
return &LangfuseHandler{langfuseService: service.NewLangfuseService()}
36+
}
37+
38+
// SetAPIKey handles POST/PUT /api/v1/langfuse/api-key.
39+
// Validates the supplied keys against Langfuse, then upserts the record.
40+
// Secret key is stored but not echoed back to the caller.
41+
// @Summary Set Langfuse API key
42+
// @Description Create or update the Langfuse credentials for the current tenant.
43+
// @Tags langfuse
44+
// @Accept json
45+
// @Produce json
46+
// @Param request body service.SetAPIKeyRequest true "Langfuse credentials"
47+
// @Success 200 {object} map[string]interface{}
48+
// @Router /api/v1/langfuse/api-key [post]
49+
func (h *LangfuseHandler) SetAPIKey(c *gin.Context) {
50+
user, code, msg := GetUser(c)
51+
if code != common.CodeSuccess {
52+
jsonError(c, code, msg)
53+
return
54+
}
55+
56+
var req service.SetAPIKeyRequest
57+
if err := c.ShouldBindJSON(&req); err != nil {
58+
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
59+
return
60+
}
61+
62+
data, err := h.langfuseService.SetAPIKey(user.ID, &req)
63+
if err != nil {
64+
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
65+
return
66+
}
67+
68+
c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": data, "message": "success"})
69+
}
70+
71+
// GetAPIKey handles GET /api/v1/langfuse/api-key.
72+
// Returns stored metadata and Langfuse project info; secret_key is never returned.
73+
// @Summary Get Langfuse API key info
74+
// @Description Retrieve the stored Langfuse credentials (without secret_key) and project info.
75+
// @Tags langfuse
76+
// @Produce json
77+
// @Success 200 {object} map[string]interface{}
78+
// @Router /api/v1/langfuse/api-key [get]
79+
func (h *LangfuseHandler) GetAPIKey(c *gin.Context) {
80+
user, code, msg := GetUser(c)
81+
if code != common.CodeSuccess {
82+
jsonError(c, code, msg)
83+
return
84+
}
85+
86+
data, err := h.langfuseService.GetAPIKey(user.ID)
87+
if err != nil {
88+
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
89+
return
90+
}
91+
if data == nil {
92+
c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": nil, "message": "Have not record any Langfuse keys."})
93+
return
94+
}
95+
96+
c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": data, "message": "success"})
97+
}
98+
99+
// DeleteAPIKey handles DELETE /api/v1/langfuse/api-key.
100+
// Removes the stored Langfuse credentials for the current tenant.
101+
// @Summary Delete Langfuse API key
102+
// @Description Remove the stored Langfuse credentials for the current tenant.
103+
// @Tags langfuse
104+
// @Produce json
105+
// @Success 200 {object} map[string]interface{}
106+
// @Router /api/v1/langfuse/api-key [delete]
107+
func (h *LangfuseHandler) DeleteAPIKey(c *gin.Context) {
108+
user, code, msg := GetUser(c)
109+
if code != common.CodeSuccess {
110+
jsonError(c, code, msg)
111+
return
112+
}
113+
114+
deleted, err := h.langfuseService.DeleteAPIKey(user.ID)
115+
if err != nil {
116+
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
117+
return
118+
}
119+
if !deleted {
120+
c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": nil, "message": "Have not record any Langfuse keys."})
121+
return
122+
}
123+
124+
c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": true, "message": "success"})
125+
}

internal/router/router.go

Lines changed: 50 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,27 @@ import (
2323
)
2424

2525
type Router struct {
26-
authHandler *handler.AuthHandler
27-
userHandler *handler.UserHandler
28-
tenantHandler *handler.TenantHandler
29-
documentHandler *handler.DocumentHandler
30-
datasetsHandler *handler.DatasetsHandler
31-
systemHandler *handler.SystemHandler
32-
knowledgebaseHandler *handler.KnowledgebaseHandler
33-
chunkHandler *handler.ChunkHandler
34-
llmHandler *handler.LLMHandler
35-
chatHandler *handler.ChatHandler
36-
chatSessionHandler *handler.ChatSessionHandler
37-
connectorHandler *handler.ConnectorHandler
38-
searchHandler *handler.SearchHandler
39-
fileHandler *handler.FileHandler
40-
memoryHandler *handler.MemoryHandler
41-
mcpHandler *handler.MCPHandler
42-
skillSearchHandler *handler.SkillSearchHandler
43-
providerHandler *handler.ProviderHandler
44-
agentHandler *handler.AgentHandler
45-
relatedQuestionsHandler *handler.SearchbotHandler
26+
authHandler *handler.AuthHandler
27+
userHandler *handler.UserHandler
28+
tenantHandler *handler.TenantHandler
29+
documentHandler *handler.DocumentHandler
30+
datasetsHandler *handler.DatasetsHandler
31+
systemHandler *handler.SystemHandler
32+
knowledgebaseHandler *handler.KnowledgebaseHandler
33+
chunkHandler *handler.ChunkHandler
34+
llmHandler *handler.LLMHandler
35+
chatHandler *handler.ChatHandler
36+
chatSessionHandler *handler.ChatSessionHandler
37+
connectorHandler *handler.ConnectorHandler
38+
searchHandler *handler.SearchHandler
39+
fileHandler *handler.FileHandler
40+
memoryHandler *handler.MemoryHandler
41+
mcpHandler *handler.MCPHandler
42+
skillSearchHandler *handler.SkillSearchHandler
43+
providerHandler *handler.ProviderHandler
44+
agentHandler *handler.AgentHandler
45+
relatedQuestionsHandler *handler.SearchbotHandler
46+
langfuseHandler *handler.LangfuseHandler
4647
}
4748

4849
// NewRouter create router
@@ -69,26 +70,27 @@ func NewRouter(
6970
relatedQuestionsHandler *handler.SearchbotHandler,
7071
) *Router {
7172
return &Router{
72-
authHandler: authHandler,
73-
userHandler: userHandler,
74-
tenantHandler: tenantHandler,
75-
documentHandler: documentHandler,
76-
datasetsHandler: datasetsHandler,
77-
systemHandler: systemHandler,
78-
knowledgebaseHandler: knowledgebaseHandler,
79-
chunkHandler: chunkHandler,
80-
llmHandler: llmHandler,
81-
chatHandler: chatHandler,
82-
chatSessionHandler: chatSessionHandler,
83-
connectorHandler: connectorHandler,
84-
searchHandler: searchHandler,
85-
fileHandler: fileHandler,
86-
memoryHandler: memoryHandler,
87-
mcpHandler: mcpHandler,
88-
skillSearchHandler: skillSearchHandler,
89-
providerHandler: providerHandler,
90-
agentHandler: agentHandler,
73+
authHandler: authHandler,
74+
userHandler: userHandler,
75+
tenantHandler: tenantHandler,
76+
documentHandler: documentHandler,
77+
datasetsHandler: datasetsHandler,
78+
systemHandler: systemHandler,
79+
knowledgebaseHandler: knowledgebaseHandler,
80+
chunkHandler: chunkHandler,
81+
llmHandler: llmHandler,
82+
chatHandler: chatHandler,
83+
chatSessionHandler: chatSessionHandler,
84+
connectorHandler: connectorHandler,
85+
searchHandler: searchHandler,
86+
fileHandler: fileHandler,
87+
memoryHandler: memoryHandler,
88+
mcpHandler: mcpHandler,
89+
skillSearchHandler: skillSearchHandler,
90+
providerHandler: providerHandler,
91+
agentHandler: agentHandler,
9192
relatedQuestionsHandler: relatedQuestionsHandler,
93+
langfuseHandler: handler.NewLangfuseHandler(),
9294
}
9395
}
9496

@@ -380,6 +382,15 @@ func (r *Router) Setup(engine *gin.Engine) {
380382

381383
}
382384

385+
// Langfuse credential management routes.
386+
langfuse := v1.Group("/langfuse")
387+
{
388+
langfuse.POST("/api-key", r.langfuseHandler.SetAPIKey)
389+
langfuse.PUT("/api-key", r.langfuseHandler.SetAPIKey)
390+
langfuse.GET("/api-key", r.langfuseHandler.GetAPIKey)
391+
langfuse.DELETE("/api-key", r.langfuseHandler.DeleteAPIKey)
392+
}
393+
383394
connector := v1.Group("/connectors")
384395
{
385396
connector.GET("/", r.connectorHandler.ListConnectors)

0 commit comments

Comments
 (0)