Skip to content

Commit d317f44

Browse files
authored
Merge pull request #956 from jupyter-naas/955-featapps-rebuild-apps-section-as-a-real-marketplace-with-installedavailable-modules
feat(apps): rebuild Apps section as a real marketplace
2 parents 4f3648c + 9d5b8cb commit d317f44

23 files changed

Lines changed: 1524 additions & 276 deletions

File tree

AGENTS.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,19 +169,33 @@ def unsupported_method(self, arg: str) -> None:
169169

170170
After `abi stack start`, the web UI is at `http://localhost:3042`.
171171

172-
Default admin credentials (set in `config.local.yaml` under `nexus_config.users`):
172+
Default admin credentials:
173173

174174
| Email | Password |
175175
|---|---|
176176
| `admin@example.com` | `Admin1234!` |
177177

178178
Password login is enabled via `auth_password_enabled: true` in `config.local.yaml`. Set it to `false` to switch back to magic link.
179179

180-
If you see "Incorrect email or password", the Postgres volume may have been initialized with a different password. Reset it:
180+
**How the password is set:** On first boot, the seeder looks for `NEXUS_USER_ADMIN_EXAMPLE_COM_PASSWORD` in `.env`. If found, it uses that value. If missing, it generates a random password and writes it back to `.env`. The `.env` in this repo ships with `Admin1234!` pre-set, so all teammates get the same password as long as they don't delete that line.
181181

182+
If you see "Incorrect email or password":
183+
184+
**Option A (no data to keep):** Wipe and reseed.
185+
```bash
186+
docker volume rm abi_postgres_data
187+
abi stack start
188+
```
189+
190+
**Option B (keep existing data):** The user already exists with a mismatched hash. Add the missing key to `.env` then force-update the hash:
182191
```bash
183-
PG_PASS=$(grep POSTGRES_PASSWORD .env | cut -d= -f2)
184-
docker exec abi-postgres-1 psql -U abi -c "ALTER USER abi PASSWORD '$PG_PASS';"
192+
# 1. Add to .env if missing:
193+
echo "NEXUS_USER_ADMIN_EXAMPLE_COM_EMAIL=admin@example.com" >> .env
194+
echo "NEXUS_USER_ADMIN_EXAMPLE_COM_PASSWORD=Admin1234!" >> .env
195+
196+
# 2. Reset the hash in Postgres directly:
197+
HASH=$(docker exec abi-abi-1 python3 -c "import bcrypt; print(bcrypt.hashpw(b'Admin1234!', bcrypt.gensalt()).decode())")
198+
docker exec abi-postgres-1 psql -U abi -d nexus -c "UPDATE users SET hashed_password='$HASH' WHERE email='admin@example.com';"
185199
docker compose restart abi
186200
```
187201

config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ modules:
3838
nexus_config:
3939
database_url: "postgresql+asyncpg://{{ secret.POSTGRES_USER }}:{{ secret.POSTGRES_PASSWORD }}@localhost:5432/nexus"
4040
redis_url: "redis://localhost:6379/0"
41+
marketplace:
42+
pricing:
43+
maintenance_standard_usd: 499
44+
maintenance_early_access_usd: 299
45+
cta_url: "https://naas.ai/enterprise"
46+
enterprise_categories: ["domain"]
4147

4248
- module: naas_abi_marketplace.ai.chatgpt
4349
enabled: true

libs/naas-abi-marketplace/naas_abi_marketplace/applications/agicap/agents/AgicapAgent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
NAME = "Agicap"
1111
DESCRIPTION = "Expert cash flow management and financial analysis agent with access to Agicap Integration tools."
12-
AVATAR_URL = "https://logo.clearbit.com/agicap.com"
12+
AVATAR_URL = "https://agicap.com/favicon.ico"
1313
SYSTEM_PROMPT = """<role>
1414
You are Agicap, an expert financial analyst and cash flow management specialist. You possess deep expertise in:
1515
</role>

libs/naas-abi-marketplace/naas_abi_marketplace/applications/pennylane/agents/PennylaneAgent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
DESCRIPTION = "A Pennylane Agent for managing accounting and financial operations."
1010
MODEL = "o3-mini"
1111
TEMPERATURE = 1
12-
AVATAR_URL = "https://logo.clearbit.com/pennylane.tech"
12+
AVATAR_URL = "https://www.pennylane.tech/favicon.ico"
1313
SYSTEM_PROMPT = """Tu es un agent Pennylane avec accès aux outils d'intégration Pennylane.
1414
Si tu n'as pas accès aux outils, demande à l'utilisateur de configurer son PENNYLANE_API_TOKEN dans le fichier .env.
1515
Sois toujours clair et professionnel dans ta communication en aidant les utilisateurs à gérer leurs données comptables.

libs/naas-abi-marketplace/naas_abi_marketplace/applications/postgres/agents/PostgresAgent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
DESCRIPTION = "A PostgreSQL Assistant for managing database operations."
1313
MODEL = "gpt-4"
1414
TEMPERATURE = 0
15-
AVATAR_URL = "https://logo.clearbit.com/postgresql.org"
15+
AVATAR_URL = "https://www.postgresql.org/media/img/about/press/elephant.png"
1616
SYSTEM_PROMPT = """
1717
## Role
1818
You are a PostgreSQL Assistant with access to PostgresIntegration tools.

libs/naas-abi/naas_abi/__init__.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,69 @@ def _initialize_nexus_service_registry() -> None:
3030
pass
3131

3232

33+
class ModelPricingEntry(BaseModel):
34+
"""LLM token cost for one model (USD per 1M tokens)."""
35+
36+
model_config = ConfigDict(extra="forbid")
37+
38+
input_per_million: float
39+
output_per_million: float
40+
label: str
41+
42+
43+
class MarketplaceUsageTier(BaseModel):
44+
"""A concrete usage scenario used to estimate monthly LLM token costs."""
45+
46+
model_config = ConfigDict(extra="forbid")
47+
48+
label: str
49+
interactions: int
50+
avg_tokens: int
51+
description: str
52+
53+
54+
class MarketplacePricingConfig(BaseModel):
55+
"""Maintenance fee configuration (expert retainer, not a license)."""
56+
57+
model_config = ConfigDict(extra="forbid")
58+
59+
maintenance_standard_usd: int = 499
60+
maintenance_early_access_usd: int = 299
61+
cta_url: str = "https://naas.ai/enterprise"
62+
enterprise_categories: list[str] = Field(default_factory=lambda: ["domain"])
63+
input_output_ratio: float = 0.6
64+
65+
66+
class MarketplaceConfig(BaseModel):
67+
"""Full marketplace configuration surfaced to the frontend via /api/modules/config."""
68+
69+
model_config = ConfigDict(extra="forbid")
70+
71+
pricing: MarketplacePricingConfig = Field(default_factory=MarketplacePricingConfig)
72+
usage_tiers: list[MarketplaceUsageTier] = Field(
73+
default_factory=lambda: [
74+
MarketplaceUsageTier(label="Starter", interactions=50, avg_tokens=2_000, description="~2 queries/day"),
75+
MarketplaceUsageTier(label="Professional", interactions=300, avg_tokens=5_000, description="~10 queries/day"),
76+
MarketplaceUsageTier(label="Scale", interactions=2_000, avg_tokens=10_000, description="~65 queries/day, team use"),
77+
]
78+
)
79+
model_pricing: dict[str, ModelPricingEntry] = Field(
80+
default_factory=lambda: {
81+
"gpt-4o": ModelPricingEntry(input_per_million=2.50, output_per_million=10.00, label="GPT-4o"),
82+
"gpt-4o-mini": ModelPricingEntry(input_per_million=0.15, output_per_million=0.60, label="GPT-4o mini"),
83+
"gpt-4": ModelPricingEntry(input_per_million=30.00, output_per_million=60.00, label="GPT-4"),
84+
"gpt-3.5-turbo": ModelPricingEntry(input_per_million=0.50, output_per_million=1.50, label="GPT-3.5 Turbo"),
85+
"o1": ModelPricingEntry(input_per_million=15.00, output_per_million=60.00, label="o1"),
86+
"o3-mini": ModelPricingEntry(input_per_million=1.10, output_per_million=4.40, label="o3-mini"),
87+
"claude-opus": ModelPricingEntry(input_per_million=15.00, output_per_million=75.00, label="Claude Opus"),
88+
"claude-sonnet": ModelPricingEntry(input_per_million=3.00, output_per_million=15.00, label="Claude Sonnet"),
89+
"claude-haiku": ModelPricingEntry(input_per_million=0.25, output_per_million=1.25, label="Claude Haiku"),
90+
"gemini-1.5-pro": ModelPricingEntry(input_per_million=3.50, output_per_million=10.50, label="Gemini 1.5 Pro"),
91+
"gemini-1.5-flash": ModelPricingEntry(input_per_million=0.075, output_per_million=0.30, label="Gemini Flash"),
92+
}
93+
)
94+
95+
3396
class TenantConfig(BaseModel):
3497
"""Tenant branding surfaced to the browser (tab title, favicon, etc.)."""
3598

@@ -242,6 +305,7 @@ class NexusConfig(BaseModel):
242305

243306
tenant: TenantConfig = Field(default_factory=TenantConfig)
244307
feature_flags: FeatureFlagsConfig = Field(default_factory=FeatureFlagsConfig)
308+
marketplace: MarketplaceConfig = Field(default_factory=MarketplaceConfig)
245309
users: list[UserSeedConfig] = Field(default_factory=list)
246310
organizations: list[OrganizationSeedConfig] = Field(default_factory=list)
247311

libs/naas-abi/naas_abi/agents/AbiAgent.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,10 @@ class AbiAgent(IntentAgent):
8585
"disabled": True,
8686
},
8787
{
88-
"label": "Browse Apps",
88+
"label": "Browse Marketplace",
8989
"value": "",
9090
"description": "Discover and enable modules from the marketplace",
91-
"cta": "/apps",
91+
"cta": "/marketplace",
9292
},
9393
]
9494

libs/naas-abi/naas_abi/apps/nexus/apps/api/app/api/router.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from naas_abi.apps.nexus.apps.api.app.services.auth.handlers import router as auth_router
2020
from naas_abi.apps.nexus.apps.api.app.services.chat.handlers import router as chat_router
2121
from naas_abi.apps.nexus.apps.api.app.services.files.handlers import router as files_router
22+
from naas_abi.apps.nexus.apps.api.app.services.modules.handlers import router as modules_router
2223
from naas_abi.apps.nexus.apps.api.app.services.providers.handlers import router as providers_router
2324
from naas_abi.apps.nexus.apps.api.app.services.workspaces.handlers import (
2425
router as workspaces_router,
@@ -39,6 +40,7 @@
3940
api_router.include_router(graph.router, prefix="/graph", tags=["graph"])
4041
api_router.include_router(view.router, prefix="/view", tags=["view"])
4142
api_router.include_router(agents_router, prefix="/agents", tags=["agents"])
43+
api_router.include_router(modules_router, prefix="/modules", tags=["modules"])
4244
api_router.include_router(files_router, prefix="/files", tags=["files"])
4345
api_router.include_router(secrets.router, prefix="/secrets", tags=["secrets"])
4446
api_router.include_router(providers_router, prefix="/providers", tags=["providers"])

libs/naas-abi/naas_abi/apps/nexus/apps/api/app/core/config.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,98 @@ class ExternalAppConfig(BaseModel):
6161
icon_emoji: str | None = None
6262

6363

64+
class ModelPricingEntry(BaseModel):
65+
"""LLM token cost for one model (USD per 1M tokens, May 2026 rates)."""
66+
67+
model_config = ConfigDict(extra="forbid")
68+
69+
input_per_million: float
70+
output_per_million: float
71+
label: str
72+
73+
74+
class MarketplaceUsageTier(BaseModel):
75+
"""A concrete usage scenario used to estimate monthly LLM token costs."""
76+
77+
model_config = ConfigDict(extra="forbid")
78+
79+
label: str
80+
interactions: int
81+
avg_tokens: int
82+
description: str
83+
84+
85+
class MarketplacePricingConfig(BaseModel):
86+
"""
87+
Maintenance fee configuration for Enterprise-tier marketplace agents.
88+
89+
These are expert retainer fees — not software licenses. They fund the
90+
AI engineers doing continuous domain knowledge curation, model migration,
91+
ontology updates, and integration patching for each agent.
92+
"""
93+
94+
model_config = ConfigDict(extra="forbid")
95+
96+
# Standard rate (live, fully maintained agents)
97+
maintenance_standard_usd: int = 499
98+
# Early-access rate (preview agents, introductory pricing)
99+
maintenance_early_access_usd: int = 299
100+
# CTA URL for Subscribe / Early Access buttons
101+
cta_url: str = "https://naas.ai/enterprise"
102+
# Module categories considered Enterprise-maintained
103+
enterprise_categories: list[str] = Field(default_factory=lambda: ["domain"])
104+
# Token split between input (system prompt + context) and output (answer)
105+
input_output_ratio: float = 0.6
106+
107+
108+
class MarketplaceConfig(BaseModel):
109+
"""Full marketplace configuration surfaced to the frontend via /api/modules/config."""
110+
111+
model_config = ConfigDict(extra="forbid")
112+
113+
pricing: MarketplacePricingConfig = Field(default_factory=MarketplacePricingConfig)
114+
115+
usage_tiers: list[MarketplaceUsageTier] = Field(
116+
default_factory=lambda: [
117+
MarketplaceUsageTier(
118+
label="Starter",
119+
interactions=50,
120+
avg_tokens=2_000,
121+
description="~2 queries/day",
122+
),
123+
MarketplaceUsageTier(
124+
label="Professional",
125+
interactions=300,
126+
avg_tokens=5_000,
127+
description="~10 queries/day",
128+
),
129+
MarketplaceUsageTier(
130+
label="Scale",
131+
interactions=2_000,
132+
avg_tokens=10_000,
133+
description="~65 queries/day, team use",
134+
),
135+
]
136+
)
137+
138+
# Prices in USD per 1M tokens (May 2026 rates).
139+
model_pricing: dict[str, ModelPricingEntry] = Field(
140+
default_factory=lambda: {
141+
"gpt-4o": ModelPricingEntry(input_per_million=2.50, output_per_million=10.00, label="GPT-4o"),
142+
"gpt-4o-mini": ModelPricingEntry(input_per_million=0.15, output_per_million=0.60, label="GPT-4o mini"),
143+
"gpt-4": ModelPricingEntry(input_per_million=30.00, output_per_million=60.00, label="GPT-4"),
144+
"gpt-3.5-turbo": ModelPricingEntry(input_per_million=0.50, output_per_million=1.50, label="GPT-3.5 Turbo"),
145+
"o1": ModelPricingEntry(input_per_million=15.00, output_per_million=60.00, label="o1"),
146+
"o3-mini": ModelPricingEntry(input_per_million=1.10, output_per_million=4.40, label="o3-mini"),
147+
"claude-opus": ModelPricingEntry(input_per_million=15.00, output_per_million=75.00, label="Claude Opus"),
148+
"claude-sonnet": ModelPricingEntry(input_per_million=3.00, output_per_million=15.00, label="Claude Sonnet"),
149+
"claude-haiku": ModelPricingEntry(input_per_million=0.25, output_per_million=1.25, label="Claude Haiku"),
150+
"gemini-1.5-pro": ModelPricingEntry(input_per_million=3.50, output_per_million=10.50, label="Gemini 1.5 Pro"),
151+
"gemini-1.5-flash": ModelPricingEntry(input_per_million=0.075, output_per_million=0.30, label="Gemini Flash"),
152+
}
153+
)
154+
155+
64156
FeatureKey = Literal["chat", "files", "agents", "knowledge", "settings"]
65157

66158

@@ -185,6 +277,9 @@ class Settings(BaseSettings):
185277
# Feature access policy consumed by workspace bootstrap responses
186278
feature_flags: FeatureFlagsConfig = Field(default_factory=FeatureFlagsConfig)
187279

280+
# Marketplace pricing, usage tiers, and model token costs
281+
marketplace: MarketplaceConfig = Field(default_factory=MarketplaceConfig)
282+
188283
# User seed configs (upserted by email on startup)
189284
users: list[UserSeedConfig] = Field(default_factory=list)
190285

libs/naas-abi/naas_abi/apps/nexus/apps/api/app/main.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,25 @@
2727
from starlette.middleware.base import BaseHTTPMiddleware
2828

2929

30+
async def _prefetch_modules_catalog() -> None:
31+
"""Warm up the marketplace module catalog in a thread-pool worker.
32+
33+
The catalog scans naas_abi_marketplace via filesystem I/O which is
34+
synchronous. Running it via run_in_executor keeps the event loop free.
35+
"""
36+
_log = logging.getLogger(__name__)
37+
try:
38+
from naas_abi.apps.nexus.apps.api.app.services.modules.service import (
39+
_build_catalog,
40+
)
41+
42+
loop = asyncio.get_running_loop()
43+
await loop.run_in_executor(None, _build_catalog)
44+
_log.info("✓ Marketplace catalog pre-populated at startup")
45+
except Exception:
46+
_log.exception("Background modules catalog pre-fetch failed (non-fatal)")
47+
48+
3049
async def _prefetch_agent_class_registry() -> None:
3150
"""Warm up the agent class registry in a thread-pool worker.
3251
@@ -85,6 +104,9 @@ async def _startup(app: FastAPI) -> None:
85104
# for all agent modules to be imported. create_task returns immediately.
86105
asyncio.create_task(_prefetch_agent_class_registry())
87106

107+
# Pre-populate the marketplace catalog so first GET /modules/ is instant.
108+
asyncio.create_task(_prefetch_modules_catalog())
109+
88110
# # Auto-start Ollama and pull default model (Qwen3-VL:2b for vision demos)
89111
# print("Checking Ollama status...")
90112
# # Gate autostart behind config flag to avoid process management in prod

0 commit comments

Comments
 (0)