Skip to content

Commit acde26f

Browse files
authored
Merge branch 'main' into feat/skills-with-imagepullsecrets
2 parents 58d8a73 + 24b6c5a commit acde26f

63 files changed

Lines changed: 4111 additions & 173 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/release.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Configuration for GitHub's auto-generated release notes.
2+
# See: https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes
3+
4+
changelog:
5+
exclude:
6+
labels:
7+
- "duplicate"
8+
- "invalid"
9+
- "wontfix"
10+
- "stale"
11+
categories:
12+
- title: "Features"
13+
labels:
14+
- "enhancement"
15+
- title: "Bug Fixes"
16+
labels:
17+
- "bug"
18+
- title: "Documentation"
19+
labels:
20+
- "documentation"
21+
- title: "Testing"
22+
labels:
23+
- "testing"
24+
- title: "Dependencies"
25+
labels:
26+
- "dependencies"
27+
- title: "Other Changes"
28+
labels:
29+
- "*"

.github/workflows/ci.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ jobs:
121121
env:
122122
OPENAI_API_KEY: fake
123123
KAGENT_HELM_EXTRA_ARGS: >-
124-
--set rbac.clusterScoped=false
125124
--set 'rbac.namespaces={kagent}'
126125
run: |
127126
# Upgrade helm to use namespace-scoped RBAC
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Conventional Commit Labels
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, edited]
6+
7+
jobs:
8+
label-by-title:
9+
runs-on: ubuntu-latest
10+
permissions:
11+
contents: read
12+
pull-requests: write
13+
steps:
14+
- uses: bcoe/conventional-release-labels@v1
15+
with:
16+
type_labels: |
17+
{
18+
"feat": "enhancement",
19+
"fix": "bug",
20+
"docs": "documentation",
21+
"test": "testing"
22+
}

DEVELOPMENT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Before you can run kagent in Kubernetes, you need to have the following tools in
1717
- **Kind** (v0.27.0+)
1818
- **kubectl** (v1.33.4+)
1919
- **Helm**
20-
- **Go** (v1.24.6+)
20+
- **Go** (v1.26.1+)
2121
- **Docker**
2222
- **Docker Buildx** (v0.23.0+)
2323
- **Make**

go/adk/pkg/a2a/server/server.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package server
33
import (
44
"context"
55
"fmt"
6+
"net"
67
"net/http"
78
"os"
89
"os/signal"
@@ -59,7 +60,7 @@ func NewA2AServer(agentCard a2atype.AgentCard, executor a2asrv.AgentExecutor, lo
5960

6061
addr := ":" + config.Port
6162
if config.Host != "" {
62-
addr = config.Host + ":" + config.Port
63+
addr = net.JoinHostPort(config.Host, config.Port)
6364
}
6465

6566
return &A2AServer{

go/adk/pkg/agent/agent.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,16 @@ func CreateLLM(ctx context.Context, m adk.Model, log logr.Logger) (adkmodel.LLM,
312312
}
313313
return models.NewAnthropicVertexAIModelWithLogger(ctx, cfg, region, project, log)
314314

315+
case *adk.SAPAICore:
316+
cfg := models.SAPAICoreConfig{
317+
Model: m.Model,
318+
BaseUrl: m.BaseUrl,
319+
ResourceGroup: m.ResourceGroup,
320+
AuthUrl: m.AuthUrl,
321+
Headers: extractHeaders(m.Headers),
322+
}
323+
return models.NewSAPAICoreModelWithLogger(cfg, log)
324+
315325
default:
316326
return nil, fmt.Errorf("unsupported model type: %s", m.GetType())
317327
}

go/adk/pkg/models/sapaicore.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package models
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"net/url"
9+
"os"
10+
"strings"
11+
"sync"
12+
"time"
13+
14+
"github.com/go-logr/logr"
15+
)
16+
17+
type SAPAICoreConfig struct {
18+
Model string
19+
BaseUrl string
20+
ResourceGroup string
21+
AuthUrl string
22+
Headers map[string]string
23+
}
24+
25+
type SAPAICoreModel struct {
26+
Config SAPAICoreConfig
27+
Logger logr.Logger
28+
29+
mu sync.Mutex
30+
token string
31+
tokenExpiresAt time.Time
32+
deploymentURL string
33+
deploymentURLAt time.Time
34+
httpClient *http.Client
35+
}
36+
37+
func NewSAPAICoreModelWithLogger(config SAPAICoreConfig, logger logr.Logger) (*SAPAICoreModel, error) {
38+
if config.BaseUrl == "" {
39+
return nil, fmt.Errorf("SAP AI Core requires base_url")
40+
}
41+
if config.ResourceGroup == "" {
42+
config.ResourceGroup = "default"
43+
}
44+
return &SAPAICoreModel{
45+
Config: config,
46+
Logger: logger,
47+
httpClient: &http.Client{Timeout: 5 * time.Minute},
48+
}, nil
49+
}
50+
51+
func (m *SAPAICoreModel) ensureToken(ctx context.Context) (string, error) {
52+
m.mu.Lock()
53+
defer m.mu.Unlock()
54+
55+
if m.token != "" && time.Now().Before(m.tokenExpiresAt.Add(-2*time.Minute)) {
56+
return m.token, nil
57+
}
58+
59+
clientID := os.Getenv("SAP_AI_CORE_CLIENT_ID")
60+
clientSecret := os.Getenv("SAP_AI_CORE_CLIENT_SECRET")
61+
if m.Config.AuthUrl == "" || clientID == "" || clientSecret == "" {
62+
return "", fmt.Errorf("SAP AI Core requires auth_url + SAP_AI_CORE_CLIENT_ID/SECRET env vars")
63+
}
64+
65+
tokenURL := strings.TrimRight(m.Config.AuthUrl, "/")
66+
if !strings.HasSuffix(tokenURL, "/oauth/token") {
67+
tokenURL += "/oauth/token"
68+
}
69+
70+
formData := url.Values{
71+
"grant_type": {"client_credentials"},
72+
"client_id": {clientID},
73+
"client_secret": {clientSecret},
74+
}
75+
req, err := http.NewRequestWithContext(ctx, "POST", tokenURL, strings.NewReader(formData.Encode()))
76+
if err != nil {
77+
return "", fmt.Errorf("failed to create OAuth2 token request: %w", err)
78+
}
79+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
80+
81+
resp, err := m.httpClient.Do(req)
82+
if err != nil {
83+
return "", fmt.Errorf("OAuth2 token request failed: %w", err)
84+
}
85+
defer resp.Body.Close()
86+
87+
if resp.StatusCode != http.StatusOK {
88+
return "", &orchHTTPError{StatusCode: resp.StatusCode, URL: tokenURL}
89+
}
90+
91+
var tokenResp struct {
92+
AccessToken string `json:"access_token"`
93+
ExpiresIn int `json:"expires_in"`
94+
}
95+
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
96+
return "", fmt.Errorf("failed to decode OAuth2 token response: %w", err)
97+
}
98+
99+
m.token = tokenResp.AccessToken
100+
if tokenResp.ExpiresIn > 0 {
101+
m.tokenExpiresAt = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
102+
} else {
103+
m.tokenExpiresAt = time.Now().Add(12 * time.Hour)
104+
}
105+
return m.token, nil
106+
}
107+
108+
func (m *SAPAICoreModel) invalidateToken() {
109+
m.mu.Lock()
110+
defer m.mu.Unlock()
111+
m.token = ""
112+
m.tokenExpiresAt = time.Time{}
113+
}
114+
115+
func (m *SAPAICoreModel) resolveDeploymentURL(ctx context.Context) (string, error) {
116+
m.mu.Lock()
117+
if m.deploymentURL != "" && time.Now().Before(m.deploymentURLAt.Add(time.Hour)) {
118+
u := m.deploymentURL
119+
m.mu.Unlock()
120+
return u, nil
121+
}
122+
m.mu.Unlock()
123+
124+
token, err := m.ensureToken(ctx)
125+
if err != nil {
126+
return "", err
127+
}
128+
129+
reqURL := fmt.Sprintf("%s/v2/lm/deployments", m.Config.BaseUrl)
130+
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
131+
if err != nil {
132+
return "", err
133+
}
134+
req.Header.Set("Authorization", "Bearer "+token)
135+
req.Header.Set("AI-Resource-Group", m.Config.ResourceGroup)
136+
137+
resp, err := m.httpClient.Do(req)
138+
if err != nil {
139+
return "", fmt.Errorf("failed to list deployments: %w", err)
140+
}
141+
defer resp.Body.Close()
142+
143+
if resp.StatusCode != http.StatusOK {
144+
return "", &orchHTTPError{StatusCode: resp.StatusCode, URL: reqURL}
145+
}
146+
147+
var result struct {
148+
Resources []struct {
149+
ID string `json:"id"`
150+
ScenarioID string `json:"scenarioId"`
151+
Status string `json:"status"`
152+
DeploymentURL string `json:"deploymentUrl"`
153+
CreatedAt string `json:"createdAt"`
154+
} `json:"resources"`
155+
}
156+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
157+
return "", fmt.Errorf("failed to decode deployments: %w", err)
158+
}
159+
160+
var best string
161+
var bestCreated string
162+
for _, d := range result.Resources {
163+
if d.ScenarioID == "orchestration" && d.Status == "RUNNING" && d.DeploymentURL != "" {
164+
if d.CreatedAt > bestCreated {
165+
best = d.DeploymentURL
166+
bestCreated = d.CreatedAt
167+
}
168+
}
169+
}
170+
if best == "" {
171+
return "", fmt.Errorf("no running orchestration deployment found in SAP AI Core")
172+
}
173+
174+
m.mu.Lock()
175+
m.deploymentURL = best
176+
m.deploymentURLAt = time.Now()
177+
m.mu.Unlock()
178+
179+
m.Logger.Info("Resolved SAP AI Core orchestration deployment", "url", best)
180+
return best, nil
181+
}
182+
183+
func (m *SAPAICoreModel) invalidateDeploymentURL() {
184+
m.mu.Lock()
185+
defer m.mu.Unlock()
186+
m.deploymentURL = ""
187+
m.deploymentURLAt = time.Time{}
188+
}

0 commit comments

Comments
 (0)