Skip to content

Commit 6e25eef

Browse files
authored
Merge pull request #50 from Approaching-AI/feat/external-service-import
Discover and import external inference services
2 parents 3ab9701 + 7fe3d30 commit 6e25eef

22 files changed

Lines changed: 1949 additions & 22 deletions

cmd/aima/main.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/jguan/aima/internal/agent"
1818
"github.com/jguan/aima/internal/cli"
1919
"github.com/jguan/aima/internal/engine"
20+
extsvc "github.com/jguan/aima/internal/external"
2021
"github.com/jguan/aima/internal/fleet"
2122
"github.com/jguan/aima/internal/hal"
2223
"github.com/jguan/aima/internal/knowledge"
@@ -1539,6 +1540,28 @@ func buildToolDeps(ac *appContext) *mcp.ToolDeps {
15391540
buildDeployDeps(ac, deps, pullModelCore, deployRunCore)
15401541
buildKnowledgeDeps(ac, deps)
15411542
buildBenchmarkDeps(ac, deps, resolveEndpoint)
1543+
externalReconciler := extsvc.NewReconciler(db, proxyServer)
1544+
deps.ScanExternalServices = func(ctx context.Context) (json.RawMessage, error) {
1545+
services, err := externalReconciler.Scan(ctx)
1546+
if err != nil {
1547+
return nil, err
1548+
}
1549+
return json.Marshal(services)
1550+
}
1551+
deps.ListExternalServices = func(ctx context.Context) (json.RawMessage, error) {
1552+
services, err := externalReconciler.List(ctx)
1553+
if err != nil {
1554+
return nil, err
1555+
}
1556+
return json.Marshal(services)
1557+
}
1558+
deps.ImportExternalService = func(ctx context.Context, idOrBaseURL string, models []string) (json.RawMessage, error) {
1559+
result, err := externalReconciler.Import(ctx, idOrBaseURL, models)
1560+
if err != nil {
1561+
return nil, err
1562+
}
1563+
return json.Marshal(result)
1564+
}
15421565

15431566
// Onboarding (multi-action MCP tool). The closures below wrap the
15441567
// internal/onboarding package entry points; scan/init/deploy collect

internal/cli/serve.go

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,17 +102,14 @@ func newServeCmd(app *App) *cobra.Command {
102102
go openclaw.StartSyncLoop(ctx, app.OpenClaw, 10*time.Second)
103103
}
104104

105-
// Auto-scan engines on startup so Explorer and other tools see
106-
// locally available engines even before a manual engine.scan call.
107-
if app.ToolDeps != nil && app.ToolDeps.ScanEngines != nil {
105+
// Auto-reconcile local assets on startup so Explorer, onboarding, and
106+
// UI views see locally available engines and models even after an
107+
// upgrade that skips the first-run onboarding scan.
108+
if app.ToolDeps != nil && (app.ToolDeps.ScanEngines != nil || app.ToolDeps.ScanModels != nil || app.ToolDeps.ScanExternalServices != nil) {
108109
go func() {
109110
scanCtx, scanCancel := context.WithTimeout(ctx, 30*time.Second)
110111
defer scanCancel()
111-
if _, err := app.ToolDeps.ScanEngines(scanCtx, "auto", false); err != nil {
112-
slog.Warn("startup engine scan failed (non-fatal)", "error", err)
113-
} else {
114-
slog.Info("startup engine scan completed")
115-
}
112+
runStartupAssetReconcile(scanCtx, app.ToolDeps)
116113
}()
117114
}
118115

@@ -228,6 +225,33 @@ func newServeCmd(app *App) *cobra.Command {
228225
return cmd
229226
}
230227

228+
func runStartupAssetReconcile(ctx context.Context, deps *mcp.ToolDeps) {
229+
if deps == nil {
230+
return
231+
}
232+
if deps.ScanEngines != nil {
233+
if _, err := deps.ScanEngines(ctx, "auto", false); err != nil {
234+
slog.Warn("startup engine scan failed (non-fatal)", "error", err)
235+
} else {
236+
slog.Info("startup engine scan completed")
237+
}
238+
}
239+
if deps.ScanModels != nil {
240+
if _, err := deps.ScanModels(ctx); err != nil {
241+
slog.Warn("startup model scan failed (non-fatal)", "error", err)
242+
} else {
243+
slog.Info("startup model scan completed")
244+
}
245+
}
246+
if deps.ScanExternalServices != nil {
247+
if _, err := deps.ScanExternalServices(ctx); err != nil {
248+
slog.Warn("startup external service scan failed (non-fatal)", "error", err)
249+
} else {
250+
slog.Info("startup external service scan completed")
251+
}
252+
}
253+
}
254+
231255
func resolveMCPProfile(mcpEnabled bool, profile string) (mcp.Profile, error) {
232256
if profile == "" {
233257
return mcp.ProfileFull, nil

internal/cli/serve_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package cli
22

33
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
47
"testing"
58

69
"github.com/jguan/aima/internal/mcp"
@@ -123,3 +126,74 @@ func TestResolveMCPProfile(t *testing.T) {
123126
})
124127
}
125128
}
129+
130+
func TestRunStartupAssetReconcileScansEnginesAndModels(t *testing.T) {
131+
t.Parallel()
132+
133+
var engineCalls int
134+
var modelCalls int
135+
var externalCalls int
136+
var gotRuntime string
137+
var gotAutoImport bool
138+
139+
deps := &mcp.ToolDeps{
140+
ScanEngines: func(ctx context.Context, runtime string, autoImport bool) (json.RawMessage, error) {
141+
engineCalls++
142+
gotRuntime = runtime
143+
gotAutoImport = autoImport
144+
return json.RawMessage(`[]`), nil
145+
},
146+
ScanModels: func(ctx context.Context) (json.RawMessage, error) {
147+
modelCalls++
148+
return json.RawMessage(`[]`), nil
149+
},
150+
ScanExternalServices: func(ctx context.Context) (json.RawMessage, error) {
151+
externalCalls++
152+
return json.RawMessage(`[]`), nil
153+
},
154+
}
155+
156+
runStartupAssetReconcile(context.Background(), deps)
157+
158+
if engineCalls != 1 {
159+
t.Fatalf("ScanEngines call count = %d, want 1", engineCalls)
160+
}
161+
if gotRuntime != "auto" || gotAutoImport {
162+
t.Fatalf("ScanEngines(%q, %v), want (auto, false)", gotRuntime, gotAutoImport)
163+
}
164+
if modelCalls != 1 {
165+
t.Fatalf("ScanModels call count = %d, want 1", modelCalls)
166+
}
167+
if externalCalls != 1 {
168+
t.Fatalf("ScanExternalServices call count = %d, want 1", externalCalls)
169+
}
170+
}
171+
172+
func TestRunStartupAssetReconcileContinuesWhenEngineScanFails(t *testing.T) {
173+
t.Parallel()
174+
175+
var modelCalls int
176+
var externalCalls int
177+
deps := &mcp.ToolDeps{
178+
ScanEngines: func(ctx context.Context, runtime string, autoImport bool) (json.RawMessage, error) {
179+
return nil, errors.New("engine scanner unavailable")
180+
},
181+
ScanModels: func(ctx context.Context) (json.RawMessage, error) {
182+
modelCalls++
183+
return json.RawMessage(`[]`), nil
184+
},
185+
ScanExternalServices: func(ctx context.Context) (json.RawMessage, error) {
186+
externalCalls++
187+
return json.RawMessage(`[]`), nil
188+
},
189+
}
190+
191+
runStartupAssetReconcile(context.Background(), deps)
192+
193+
if modelCalls != 1 {
194+
t.Fatalf("ScanModels call count = %d, want 1", modelCalls)
195+
}
196+
if externalCalls != 1 {
197+
t.Fatalf("ScanExternalServices call count = %d, want 1", externalCalls)
198+
}
199+
}

0 commit comments

Comments
 (0)