create-stellar-service 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +142 -2
- package/dist/files.d.ts +133 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +374 -0
- package/dist/files.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/kit.d.ts +28 -0
- package/dist/kit.d.ts.map +1 -0
- package/dist/kit.js +44 -0
- package/dist/kit.js.map +1 -0
- package/dist/main.d.ts +47 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +510 -0
- package/dist/main.js.map +1 -0
- package/package.json +24 -4
- package/templates/fly/fly.toml +36 -0
- package/templates/gateway/Dockerfile +24 -0
- package/templates/python/Dockerfile +27 -0
- package/templates/python/app.py +39 -0
- package/templates/python/requirements.txt +2 -0
- package/templates/python/start.sh +16 -0
- package/templates/typescript/Dockerfile +31 -0
- package/templates/typescript/src/server.ts +57 -0
- package/templates/typescript/src/services/summarize.ts +47 -0
- package/templates/typescript/tsconfig.json +14 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# {{name}}: the FastAPI app and the Provider Kit gateway in one container. The app listens on 127.0.0.1:8000, inside
|
|
2
|
+
# the container only, and the gateway on port 8787, in front of it.
|
|
3
|
+
#
|
|
4
|
+
# docker build -t {{package}} .
|
|
5
|
+
# docker run -p 8787:8787 -v {{package}}-jobs:/data -e PAY_TO=G... -e PUBLIC_ORIGIN=https://your.domain {{package}}
|
|
6
|
+
#
|
|
7
|
+
# Paid jobs are kept in /data/jobs.sqlite: give /data a persistent volume. The image holds no Stellar secret key.
|
|
8
|
+
# fly.toml deploys it on Fly.io: npm run launch does, or npm run deploy.
|
|
9
|
+
|
|
10
|
+
FROM node:24-slim
|
|
11
|
+
RUN apt-get update && apt-get install -y --no-install-recommends python3 python3-venv && rm -rf /var/lib/apt/lists/*
|
|
12
|
+
ENV NODE_ENV=production JOB_DB_PATH=/data/jobs.sqlite PATH=/opt/venv/bin:$PATH
|
|
13
|
+
WORKDIR /app
|
|
14
|
+
COPY requirements.txt ./
|
|
15
|
+
RUN python3 -m venv /opt/venv && pip install --no-cache-dir -r requirements.txt
|
|
16
|
+
COPY package.json package-lock.json ./
|
|
17
|
+
{{vendor}}
|
|
18
|
+
RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force
|
|
19
|
+
COPY app.py provider.config.json start.sh ./
|
|
20
|
+
RUN mkdir -p /data && chown node:node /data
|
|
21
|
+
VOLUME /data
|
|
22
|
+
EXPOSE 8787
|
|
23
|
+
HEALTHCHECK --interval=30s --timeout=5s CMD node -e "fetch('http://127.0.0.1:' + (process.env.PORT || 8787) + '/healthz').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))"
|
|
24
|
+
# A host's new volume may belong to root, as Fly.io's do: started as root, the container only gives /data to the node
|
|
25
|
+
# user, then runs the service as node. Started as another user, it runs as that user.
|
|
26
|
+
ENTRYPOINT ["sh", "-c", "if [ \"$(id -u)\" = 0 ]; then chown node:node /data 2>/dev/null || true; exec setpriv --reuid=node --regid=node --init-groups -- \"$@\"; fi; exec \"$@\"", "sh"]
|
|
27
|
+
CMD ["sh", "start.sh"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""{{name}}: the service itself, a FastAPI app that knows nothing about payments.
|
|
2
|
+
|
|
3
|
+
The Provider Kit gateway (provider.config.json) stands in front of it. It publishes the manifest, takes the USDC
|
|
4
|
+
payment, queues jobs and keeps them for recovery, and POSTs only validated input here. Keep the models below in step
|
|
5
|
+
with the schemas in provider.config.json: agents read those before they pay.
|
|
6
|
+
|
|
7
|
+
`npm run dev` starts it next to the gateway: {{apiCommand}}
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from fastapi import FastAPI
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
app = FastAPI(title={{nameJson}})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SummarizeIn(BaseModel):
|
|
19
|
+
text: str = Field(min_length=1, max_length=20000, description="The text to summarize")
|
|
20
|
+
sentences: int = Field(default=1, ge=1, le=5, description="How many sentences to keep")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SummarizeOut(BaseModel):
|
|
24
|
+
summary: str
|
|
25
|
+
sentences: int = Field(description="Sentences in the text")
|
|
26
|
+
words: int = Field(description="Words in the text")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.post(
|
|
30
|
+
"/summarize",
|
|
31
|
+
summary="Summarize",
|
|
32
|
+
description="Returns the first sentences of an English text, with its sentence and word counts. Use it to shorten "
|
|
33
|
+
"text before passing it on. Accepts up to 20,000 characters; it does not paraphrase.",
|
|
34
|
+
)
|
|
35
|
+
def summarize(body: SummarizeIn) -> SummarizeOut:
|
|
36
|
+
# Your own work goes here, such as a model call. The gateway sends the job id in the Idempotency-Key header, so
|
|
37
|
+
# a job it runs again after a restart can be recognized.
|
|
38
|
+
parts = [part.strip() for part in re.split(r"(?<=[.!?])\s+", body.text) if part.strip()]
|
|
39
|
+
return SummarizeOut(summary=" ".join(parts[: body.sentences]), sentences=len(parts), words=len(body.text.split()))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Starts the FastAPI app and the gateway in front of it. When either one stops, so does the other, and the container
|
|
3
|
+
# exits for its host to restart. A stop signal reaches both: the gateway finishes the requests in flight.
|
|
4
|
+
|
|
5
|
+
uvicorn app:app --host 127.0.0.1 --port 8000 &
|
|
6
|
+
api=$!
|
|
7
|
+
node node_modules/@stellar-agent-services/provider-kit/dist/cli/index.js serve --config provider.config.json &
|
|
8
|
+
gateway=$!
|
|
9
|
+
|
|
10
|
+
asked=0
|
|
11
|
+
trap 'asked=1; kill -TERM "$api" "$gateway" 2>/dev/null' TERM INT
|
|
12
|
+
while kill -0 "$api" 2>/dev/null && kill -0 "$gateway" 2>/dev/null; do sleep 1; done
|
|
13
|
+
kill -TERM "$api" "$gateway" 2>/dev/null
|
|
14
|
+
wait
|
|
15
|
+
# 0 when asked to stop; 1 when one of them stopped by itself.
|
|
16
|
+
[ "$asked" = 1 ]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# {{name}}: the service and its job database, on any host that runs a container.
|
|
2
|
+
#
|
|
3
|
+
# docker build -t {{package}} .
|
|
4
|
+
# docker run -p 8787:8787 -v {{package}}-jobs:/data -e PAY_TO=G... -e PUBLIC_ORIGIN=https://your.domain {{package}}
|
|
5
|
+
#
|
|
6
|
+
# Paid jobs are kept in /data/jobs.sqlite: give /data a persistent volume. The image holds no Stellar secret key.
|
|
7
|
+
# fly.toml deploys it on Fly.io: npm run launch does, or npm run deploy.
|
|
8
|
+
|
|
9
|
+
FROM node:24-slim AS build
|
|
10
|
+
WORKDIR /app
|
|
11
|
+
COPY package.json package-lock.json tsconfig.json ./
|
|
12
|
+
{{vendor}}
|
|
13
|
+
RUN npm ci --ignore-scripts
|
|
14
|
+
COPY src ./src
|
|
15
|
+
RUN npx tsc
|
|
16
|
+
|
|
17
|
+
FROM node:24-slim
|
|
18
|
+
ENV NODE_ENV=production JOB_DB_PATH=/data/jobs.sqlite
|
|
19
|
+
WORKDIR /app
|
|
20
|
+
COPY package.json package-lock.json ./
|
|
21
|
+
{{vendor}}
|
|
22
|
+
RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force
|
|
23
|
+
COPY --from=build /app/dist ./dist
|
|
24
|
+
RUN mkdir -p /data && chown node:node /data
|
|
25
|
+
VOLUME /data
|
|
26
|
+
EXPOSE 8787
|
|
27
|
+
HEALTHCHECK --interval=30s --timeout=5s CMD node -e "fetch('http://127.0.0.1:' + (process.env.PORT || 8787) + '/healthz').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))"
|
|
28
|
+
# A host's new volume may belong to root, as Fly.io's do: started as root, the container only gives /data to the node
|
|
29
|
+
# user, then runs the service as node. Started as another user, it runs as that user.
|
|
30
|
+
ENTRYPOINT ["sh", "-c", "if [ \"$(id -u)\" = 0 ]; then chown node:node /data 2>/dev/null || true; exec setpriv --reuid=node --regid=node --init-groups -- \"$@\"; fi; exec \"$@\"", "sh"]
|
|
31
|
+
CMD ["node", "--disable-warning=ExperimentalWarning", "dist/server.js"]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// {{name}}: a paid service on Stellar, built with the Provider Kit (Provider Standard v1).
|
|
2
|
+
//
|
|
3
|
+
// Settings come from .env: public values, and on mainnet the facilitator's API key. This service never needs a Stellar
|
|
4
|
+
// secret key. Paid jobs are kept in SQLite (JOB_DB_PATH), so they survive a restart, and a client that lost its
|
|
5
|
+
// response can still collect the result. Add services in src/services/ and list them below. `npm run launch` starts
|
|
6
|
+
// this file with PUBLIC_ORIGIN set to the public address, to read the manifest it will serve there.
|
|
7
|
+
|
|
8
|
+
import { createProvider, httpFacilitator, listen, sqliteStore, type ProviderConfig } from "@stellar-agent-services/provider-kit";
|
|
9
|
+
import { summarize } from "./services/summarize.js";
|
|
10
|
+
|
|
11
|
+
const port = Number(process.env["PORT"] ?? "8787");
|
|
12
|
+
const origin = process.env["PUBLIC_ORIGIN"] ?? `http://localhost:${port}`;
|
|
13
|
+
const dev = origin.startsWith("http://localhost");
|
|
14
|
+
// stellar:testnet takes test money; stellar:pubnet is mainnet, real USDC.
|
|
15
|
+
const network = (process.env["STELLAR_NETWORK"] ?? "stellar:testnet") as ProviderConfig["network"];
|
|
16
|
+
const payTo = process.env["PAY_TO"];
|
|
17
|
+
if (!payTo) throw new Error(`PAY_TO is empty: npm run launch sets up your own wallet${network === "stellar:testnet" ? ", and npm run dev makes a throwaway testnet account meanwhile" : ""}.`);
|
|
18
|
+
const agentId = process.env["AGENT_ID"];
|
|
19
|
+
const rpcUrl = process.env["SOROBAN_RPC_URL"];
|
|
20
|
+
|
|
21
|
+
// The facilitator verifies and settles payments: x402.org's on testnet, OpenZeppelin's on mainnet, which wants an API
|
|
22
|
+
// key. FACILITATOR_AUTH is sent as the Authorization header: "Bearer <key>".
|
|
23
|
+
const facilitatorUrl = process.env["FACILITATOR_URL"] ?? (network === "stellar:pubnet" ? "https://channels.openzeppelin.com/x402" : "https://x402.org/facilitator");
|
|
24
|
+
const facilitatorAuth = process.env["FACILITATOR_AUTH"];
|
|
25
|
+
if (network === "stellar:pubnet" && !facilitatorAuth && !process.env["FACILITATOR_URL"]) {
|
|
26
|
+
throw new Error('FACILITATOR_AUTH is empty: get an API key at https://channels.openzeppelin.com/gen, and set FACILITATOR_AUTH="Bearer <key>" in .env.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const provider = createProvider({
|
|
30
|
+
baseUrl: origin,
|
|
31
|
+
dev,
|
|
32
|
+
network,
|
|
33
|
+
payTo,
|
|
34
|
+
facilitator: httpFacilitator(facilitatorUrl, { dev, ...(facilitatorAuth ? { headers: { Authorization: facilitatorAuth } } : {}) }),
|
|
35
|
+
// Search matches only this name and description: use the words buyers type.
|
|
36
|
+
provider: {
|
|
37
|
+
name: {{nameJson}},
|
|
38
|
+
description: {{descriptionJson}},
|
|
39
|
+
dataUse: { storesInputs: false, trainsOnInputs: false },
|
|
40
|
+
...(agentId ? { agentId: Number(agentId) } : {}),
|
|
41
|
+
},
|
|
42
|
+
services: [summarize],
|
|
43
|
+
store: sqliteStore(process.env["JOB_DB_PATH"] ?? "./jobs.sqlite"),
|
|
44
|
+
// Decides payment_uncertain jobs from the chain. Testnet defaults to the public RPC; mainnet needs one.
|
|
45
|
+
...(rpcUrl ? { rpc: rpcUrl } : {}),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Fails fast if the facilitator can't settle this network, and resumes jobs a restart left behind.
|
|
49
|
+
await provider.ready();
|
|
50
|
+
const server = await listen(provider, { port });
|
|
51
|
+
console.log(`Listening on ${origin}`);
|
|
52
|
+
console.log(`Manifest: ${provider.manifestUrl}`);
|
|
53
|
+
console.log(`payTo: ${payTo}`);
|
|
54
|
+
|
|
55
|
+
for (const signal of ["SIGINT", "SIGTERM"] as const) {
|
|
56
|
+
process.once(signal, () => void server.close().then(() => process.exit(0)));
|
|
57
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// summarize: {{price}} USDC, charged only if it succeeds. Replace the handler with your own work, such as a model call,
|
|
2
|
+
// and keep the description, schemas and example in step with it: agents read them before they pay.
|
|
3
|
+
|
|
4
|
+
import { defineService } from "@stellar-agent-services/provider-kit";
|
|
5
|
+
|
|
6
|
+
type Input = { text: string; sentences?: number };
|
|
7
|
+
|
|
8
|
+
export const summarize = defineService({
|
|
9
|
+
name: "summarize",
|
|
10
|
+
title: "Summarize",
|
|
11
|
+
description:
|
|
12
|
+
"Returns the first sentences of an English text, with its sentence and word counts. Use it to shorten text before passing it on. Accepts up to 20,000 characters; it does not paraphrase.",
|
|
13
|
+
price: "{{price}}",
|
|
14
|
+
charge: "on-success",
|
|
15
|
+
expectedDurationSeconds: 1,
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: "object",
|
|
18
|
+
required: ["text"],
|
|
19
|
+
properties: {
|
|
20
|
+
text: { type: "string", minLength: 1, maxLength: 20000, description: "The text to summarize" },
|
|
21
|
+
sentences: { type: "integer", minimum: 1, maximum: 5, default: 1, description: "How many sentences to keep" },
|
|
22
|
+
},
|
|
23
|
+
additionalProperties: false,
|
|
24
|
+
},
|
|
25
|
+
outputSchema: {
|
|
26
|
+
type: "object",
|
|
27
|
+
required: ["summary", "sentences", "words"],
|
|
28
|
+
properties: {
|
|
29
|
+
summary: { type: "string" },
|
|
30
|
+
sentences: { type: "integer", description: "Sentences in the text" },
|
|
31
|
+
words: { type: "integer", description: "Words in the text" },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
examples: [
|
|
35
|
+
{
|
|
36
|
+
input: { text: "Stellar settles in seconds. Fees are tiny.", sentences: 1 },
|
|
37
|
+
output: { summary: "Stellar settles in seconds.", sentences: 2, words: 7 },
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
handler: ({ text, sentences = 1 }: Input) => {
|
|
41
|
+
const parts = text
|
|
42
|
+
.split(/(?<=[.!?])\s+/)
|
|
43
|
+
.map((part) => part.trim())
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
return { summary: parts.slice(0, sentences).join(" "), sentences: parts.length, words: text.split(/\s+/).filter(Boolean).length };
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2023",
|
|
4
|
+
"lib": ["ES2023"],
|
|
5
|
+
"module": "NodeNext",
|
|
6
|
+
"moduleResolution": "NodeNext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"rootDir": "src",
|
|
11
|
+
"outDir": "dist"
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|