1"""

2 AgentOS Marketplace — Agent template registry and discovery hub.

3

4 v1.14.4: Central marketplace for discovering, publishing, and installing

5 agent templates, workflows, and plugins.

6

7 Key features:

8- Template registry with semantic search

9- Versioned agent templates with dependency resolution

10- Public + private registries

11- One-click install from marketplace

12- Agent ratings, reviews, and usage stats

13- Template validation and compatibility checking

14- CLI and programmatic API

15"""

16

17 import asyncio

18 import hashlib

19 import json

20 import logging

21 import os

22 import shutil

23 import tempfile

24 import time

25 from abc import ABC, abstractmethod

26 from dataclasses import dataclass, field

27 from enum import Enum, auto

28 from pathlib import Path

29 from typing import Any, AsyncIterator, Callable, Dict, List, Optional, Set, Tuple, Union

30 from urllib.parse import urlparse

31

32 logger = logging.getLogger(__name__)

33

34

35# ---------------------------------------------------------------------------

36# Data types

37# ---------------------------------------------------------------------------

38

39 class TemplateCategory(Enum):

40 CHAT = "chat"

41 CODING = "coding"

42 ANALYSIS = "analysis"

43 AUTOMATION = "automation"

44 RESEARCH = "research"

45 CREATIVE = "creative"

46 ENTERPRISE = "enterprise"

47 UTILITY = "utility"

48 OTHER = "other"

49

50

51 class TemplateStatus(Enum):

52 DRAFT = "draft"

53 PUBLISHED = "published"

54 DEPRECATED = "deprecated"

55 ARCHIVED = "archived"

56 UNDER_REVIEW = "under_review"

57

58

59@dataclass

60 class TemplateDependency:

61 """A dependency required by a template."""

62 name: str

63 version_spec: str = "*" # PEP 440 version specifier

64 optional: bool = False

65 description: str = ""

66

67

68@dataclass

69 class TemplateVersion:

70 """A specific version of a template."""

71 version: str # SemVer

72 changelog: str = ""

73 min_agentos_version: str = "1.0.0"

74 files: Dict[str, str] = field(default_factory=dict) # path → content

75 dependencies: List[TemplateDependency] = field(default_factory=list)

76 metadata: Dict[str, Any] = field(default_factory=dict)

77 published_at: float = 0.0

78 download_count: int = 0

79

80

81@dataclass

82 class TemplateReview:

83 """User review of a template."""

84 user_id: str

85 rating: float # 1.0 - 5.0

86 comment: str = ""

87 timestamp: float = field(default_factory=time.time)

88 helpful_count: int = 0

89

90

91@dataclass

92 class AgentTemplate:

93 """An agent template in the marketplace."""

94 # Identity

95 id: str

96 name: str

97 version: str

98 author: str

99 description: str = ""

100 category: TemplateCategory = TemplateCategory.OTHER

101 tags: List[str] = field(default_factory=list)

102 icon_url: str = ""

103

104 # Status

105 status: TemplateStatus = TemplateStatus.PUBLISHED

106

107 # Content

108 versions: List[TemplateVersion] = field(default_factory=list)

109 readme: str = ""

110 license: str = "MIT"

111

112 # Engagement

113 stars: int = 0

114 downloads: int = 0

115 reviews: List[TemplateReview] = field(default_factory=list)

116

117 # Compatibility

118 compatible_agentos_versions: str = ">=1.0.0"

119 requires: List[TemplateDependency] = field(default_factory=list)

120

121 # Metadata

122 created_at: float = field(default_factory=time.time)

123 updated_at: float = field(default_factory=time.time)

124 source_url: str = ""

125 documentation_url: str = ""

126 metadata: Dict[str, Any] = field(default_factory=dict)

127

128 @property

129 def rating(self) -> float:

130 """Average rating."""

131 if not self.reviews:

132 return 0.0

133 return sum(r.rating for r in self.reviews) / len(self.reviews)

134

135 @property

136 def latest_version(self) -> Optional[TemplateVersion]:

137 """Get the latest published version."""

138 published = [v for v in self.versions if v.published_at > 0]

139 if not published:

140 return None

141 return max(published, key=lambda v: v.published_at)

142

143

144@dataclass

145 class MarketSearchQuery:

146 """Search query for the marketplace."""

147 keywords: str = ""

148 category: Optional[TemplateCategory] = None

149 tags: List[str] = field(default_factory=list)

150 min_rating: float = 0.0

151 min_stars: int = 0

152 author: Optional[str] = None

153 sort_by: str = "relevance" # relevance, downloads, rating, stars, updated

154 sort_order: str = "desc"

155 limit: int = 20

156 offset: int = 0

157

158

159@dataclass

160 class MarketSearchResult:

161 """Search result from the marketplace."""

162 template: AgentTemplate

163 score: float = 0.0

164 matched_tags: List[str] = field(default_factory=list)

165

166

167# ---------------------------------------------------------------------------

168# Registry backends

169# ---------------------------------------------------------------------------

170

171 class MarketRegistryBackend(ABC):

172 """Abstract backend for template storage and retrieval."""

173

174 @abstractmethod

175 async def list_templates(

176 self, query: Optional[MarketSearchQuery] = None

177 ) -> List[MarketSearchResult]:

178 ...

179

180 @abstractmethod

181 async def get_template(self, template_id: str) -> Optional[AgentTemplate]:

182 ...

183

184 @abstractmethod

185 async def publish_template(self, template: AgentTemplate) -> bool:

186 ...

187

188 @abstractmethod

189 async def unpublish_template(self, template_id: str) -> bool:

190 ...

191

192 @abstractmethod

193 async def add_review(self, template_id: str, review: TemplateReview) -> bool:

194 ...

195

196 @abstractmethod

197 async def get_stats(self) -> Dict[str, Any]:

198 ...

199

200

201 class InMemoryMarketBackend(MarketRegistryBackend):

202 """In-memory registry for development and testing."""

203

204 def __init__(self):

205 self._templates: Dict[str, AgentTemplate] = {}

206

207 async def list_templates(

208 self, query: Optional[MarketSearchQuery] = None

209 ) -> List[MarketSearchResult]:

210 results = []

211 for tpl in self._templates.values():

212 if tpl.status != TemplateStatus.PUBLISHED:

213 continue

214

215 score = 0.0

216 matched_tags = []

217

218 if query:

219 # Keyword search

220 if query.keywords:

221 kw_lower = query.keywords.lower()

222 text = f"{tpl.name} {tpl.description} {' '.join(tpl.tags)}".lower()

223 if kw_lower in text:

224 score += 10.0

225

226 # Category filter

227 if query.category and tpl.category != query.category:

228 continue

229

230 # Tag filter

231 if query.tags:

232 matched_tags = [t for t in query.tags if t in tpl.tags]

233 if not matched_tags:

234 continue

235 score += len(matched_tags) * 2.0

236

237 # Rating filter

238 if query.min_rating > 0 and tpl.rating < query.min_rating:

239 continue

240

241 # Stars filter

242 if query.min_stars > 0 and tpl.stars < query.min_stars:

243 continue

244

245 # Author filter

246 if query.author and tpl.author != query.author:

247 continue

248

249 results.append(MarketSearchResult(

250 template=tpl,

251 score=score,

252 matched_tags=matched_tags,

253 ))

254

255 # Sort

256 if query:

257 sort_key = query.sort_by

258 reverse = query.sort_order == "desc"

259 if sort_key == "downloads":

260 results.sort(key=lambda r: r.template.downloads, reverse=reverse)

261 elif sort_key == "rating":

262 results.sort(key=lambda r: r.template.rating, reverse=reverse)

263 elif sort_key == "stars":

264 results.sort(key=lambda r: r.template.stars, reverse=reverse)

265 elif sort_key == "updated":

266 results.sort(key=lambda r: r.template.updated_at, reverse=reverse)

267 else: # relevance

268 results.sort(key=lambda r: r.score, reverse=reverse)

269

270 # Paginate

271 results = results[query.offset:query.offset + query.limit]

272

273 return results

274

275 async def get_template(self, template_id: str) -> Optional[AgentTemplate]:

276 return self._templates.get(template_id)

277

278 async def publish_template(self, template: AgentTemplate) -> bool:

279 template.updated_at = time.time()

280 self._templates[template.id] = template

281 return True

282

283 async def unpublish_template(self, template_id: str) -> bool:

284 if template_id in self._templates:

285 self._templates[template_id].status = TemplateStatus.ARCHIVED

286 return True

287 return False

288

289 async def add_review(self, template_id: str, review: TemplateReview) -> bool:

290 tpl = self._templates.get(template_id)

291 if not tpl:

292 return False

293 tpl.reviews.append(review)

294 return True

295

296 async def get_stats(self) -> Dict[str, Any]:

297 total = len(self._templates)

298 by_category = {}

299 for tpl in self._templates.values():

300 cat = tpl.category.value

301 by_category[cat] = by_category.get(cat, 0) + 1

302 return {

303 "total_templates": total,

304 "total_downloads": sum(t.downloads for t in self._templates.values()),

305 "by_category": by_category,

306 }

307

308

309 class FileMarketBackend(MarketRegistryBackend):

310 """JSON-file-based registry for local/CI usage."""

311

312 def __init__(self, storage_dir: Union[str, Path]):

313 self._dir = Path(storage_dir)

314 self._dir.mkdir(parents=True, exist_ok=True)

315 self._index_file = self._dir / "index.json"

316 self._templates: Dict[str, AgentTemplate] = {}

317 self._load()

318

319 def _load(self) -> None:

320 if self._index_file.exists():

321 with open(self._index_file, "r") as f:

322 data = json.load(f)

323 for raw in data.get("templates", []):

324 tpl = self._dict_to_template(raw)

325 self._templates[tpl.id] = tpl

326

327 def _save(self) -> None:

328 data = {

329 "updated_at": time.time(),

330 "templates": [self._template_to_dict(t) for t in self._templates.values()],

331 }

332 with open(self._index_file, "w") as f:

333 json.dump(data, f, indent=2, default=str)

334

335 def _template_to_dict(self, tpl: AgentTemplate) -> Dict[str, Any]:

336 return {

337 "id": tpl.id,

338 "name": tpl.name,

339 "version": tpl.version,

340 "author": tpl.author,

341 "description": tpl.description,

342 "category": tpl.category.value,

343 "tags": tpl.tags,

344 "status": tpl.status.value,

345 "stars": tpl.stars,

346 "downloads": tpl.downloads,

347 "rating": tpl.rating,

348 "review_count": len(tpl.reviews),

349 "compatible_agentos_versions": tpl.compatible_agentos_versions,

350 "created_at": tpl.created_at,

351 "updated_at": tpl.updated_at,

352 }

353

354 def _dict_to_template(self, d: Dict[str, Any]) -> AgentTemplate:

355 return AgentTemplate(

356 id=d["id"],

357 name=d["name"],

358 version=d.get("version", "1.0.0"),

359 author=d["author"],

360 description=d.get("description", ""),

361 category=TemplateCategory(d.get("category", "other")),

362 tags=d.get("tags", []),

363 status=TemplateStatus(d.get("status", "published")),

364 stars=d.get("stars", 0),

365 downloads=d.get("downloads", 0),

366 compatible_agentos_versions=d.get("compatible_agentos_versions", ">=1.0.0"),

367 created_at=d.get("created_at", 0),

368 updated_at=d.get("updated_at", 0),

369 )

370

371 # Delegate to in-memory backend

372 async def list_templates(self, query=None):

373 backend = InMemoryMarketBackend()

374 backend._templates = dict(self._templates)

375 return await backend.list_templates(query)

376

377 async def get_template(self, template_id):

378 return self._templates.get(template_id)

379

380 async def publish_template(self, template):

381 tpl = AgentTemplate(**{

382 k: v for k, v in template.__dict__.items()

383 if k in AgentTemplate.__dataclass_fields__

384 })

385 template.updated_at = time.time()

386 self._templates[template.id] = template

387 self._save()

388 return True

389

390 async def unpublish_template(self, template_id):

391 if template_id in self._templates:

392 self._templates[template_id].status = TemplateStatus.ARCHIVED

393 self._save()

394 return True

395 return False

396

397 async def add_review(self, template_id, review):

398 tpl = self._templates.get(template_id)

399 if not tpl:

400 return False

401 tpl.reviews.append(review)

402 self._save()

403 return True

404

405 async def get_stats(self):

406 backend = InMemoryMarketBackend()

407 backend._templates = dict(self._templates)

408 return await backend.get_stats()

409

410

411# ---------------------------------------------------------------------------

412# Remote registry client

413# ---------------------------------------------------------------------------

414

415 class RemoteMarketClient:

416 """HTTP client for remote marketplace registries."""

417

418 def __init__(self, base_url: str, api_key: Optional[str] = None):

419 self.base_url = base_url.rstrip("/")

420 self.api_key = api_key

421

422 async def search(self, query: MarketSearchQuery) -> List[MarketSearchResult]:

423 """Search the remote marketplace."""

424 import urllib.request

425 import urllib.parse

426

427 params = {}

428 if query.keywords:

429 params["q"] = query.keywords

430 if query.category:

431 params["category"] = query.category.value

432 if query.tags:

433 params["tags"] = ",".join(query.tags)

434 if query.limit:

435 params["limit"] = str(query.limit)

436

437 url = f"{self.base_url}/api/v1/templates"

438 if params:

439 url += "?" + urllib.parse.urlencode(params)

440

441 # Use asyncio-compatible HTTP

442 import aiohttp

443 async with aiohttp.ClientSession() as session:

444 headers = {}

445 if self.api_key:

446 headers["Authorization"] = f"Bearer {self.api_key}"

447 async with session.get(url, headers=headers) as resp:

448 data = await resp.json()

449 return [

450 MarketSearchResult(

451 template=AgentTemplate(**item["template"]),

452 score=item.get("score", 0),

453 )

454 for item in data.get("results", [])

455 ]

456

457 async def get_template(self, template_id: str) -> Optional[AgentTemplate]:

458 """Fetch a template from the remote registry."""

459 import aiohttp

460 async with aiohttp.ClientSession() as session:

461 headers = {}

462 if self.api_key:

463 headers["Authorization"] = f"Bearer {self.api_key}"

464 async with session.get(

465 f"{self.base_url}/api/v1/templates/{template_id}",

466 headers=headers,

467 ) as resp:

468 if resp.status == 404:

469 return None

470 data = await resp.json()

471 return AgentTemplate(**data)

472

473 async def download_template(

474 self, template_id: str, target_dir: Union[str, Path]

475 ) -> bool:

476 """Download and extract a template to a local directory."""

477 import aiohttp

478 target = Path(target_dir)

479 target.mkdir(parents=True, exist_ok=True)

480

481 async with aiohttp.ClientSession() as session:

482 headers = {}

483 if self.api_key:

484 headers["Authorization"] = f"Bearer {self.api_key}"

485 async with session.get(

486 f"{self.base_url}/api/v1/templates/{template_id}/download",

487 headers=headers,

488 ) as resp:

489 if resp.status != 200:

490 return False

491

492 import tarfile

493 import io

494 data = await resp.read()

495 with tarfile.open(fileobj=io.BytesIO(data)) as tar:

496 tar.extractall(path=target)

497 return True

498

499

500# ---------------------------------------------------------------------------

501# Marketplace Manager

502# ---------------------------------------------------------------------------

503

504 class MarketplaceManager:

505 """Central marketplace management — search, install, publish."""

506

507 def __init__(

508 self,

509 local_backend: Optional[MarketRegistryBackend] = None,

510 remote_clients: Optional[List[RemoteMarketClient]] = None,

511 install_dir: Union[str, Path] = "~/.agentos/templates",

512 ):

513 self.local = local_backend or InMemoryMarketBackend()

514 self.remote_clients = remote_clients or []

515 self.install_dir = Path(install_dir).expanduser()

516 self.install_dir.mkdir(parents=True, exist_ok=True)

517

518 async def search(

519 self, query: MarketSearchQuery, include_remote: bool = True

520 ) -> List[MarketSearchResult]:

521 """Search local and remote registries."""

522 results = await self.local.list_templates(query)

523

524 if include_remote:

525 for client in self.remote_clients:

526 try:

527 remote_results = await client.search(query)

528 results.extend(remote_results)

529 except Exception as e:

530 logger.warning(f"Remote search failed: {e}")

531

532 # Deduplicate by template ID

533 seen: Set[str] = set()

534 deduped = []

535 for r in results:

536 if r.template.id not in seen:

537 seen.add(r.template.id)

538 deduped.append(r)

539

540 return deduped

541

542 async def install(self, template_id: str, version: Optional[str] = None) -> Path:

543 """Install a template from local or remote registry."""

544 # Check local first

545 tpl = await self.local.get_template(template_id)

546

547 # Try remote

548 if not tpl:

549 for client in self.remote_clients:

550 try:

551 tpl = await client.get_template(template_id)

552 if tpl:

553 break

554 except Exception:

555 continue

556

557 if not tpl:

558 raise ValueError(f"Template '{template_id}' not found in any registry")

559

560 # Install to local directory

561 tpl_dir = self.install_dir / template_id

562 if version:

563 tpl_dir = tpl_dir / version

564

565 tpl_dir.mkdir(parents=True, exist_ok=True)

566

567 # Write template files

568 latest = tpl.latest_version

569 if latest:

570 for filepath, content in latest.files.items():

571 full_path = tpl_dir / filepath

572 full_path.parent.mkdir(parents=True, exist_ok=True)

573 with open(full_path, "w") as f:

574 f.write(content)

575

576 # Record installation

577 tpl.downloads += 1

578 tpl.updated_at = time.time()

579

580 return tpl_dir

581

582 async def publish(

583 self,

584 template: AgentTemplate,

585 to_remote: bool = False,

586 ) -> bool:

587 """Publish a template to registries."""

588 # Always publish to local

589 ok = await self.local.publish_template(template)

590 if not ok:

591 return False

592

593 # Optionally push to remote

594 if to_remote:

595 for client in self.remote_clients:

596 try:

597 # Remote publishing would use a POST endpoint

598 pass

599 except Exception as e:

600 logger.error(f"Remote publish failed: {e}")

601

602 return True

603

604 async def get_stats(self) -> Dict[str, Any]:

605 """Get marketplace statistics."""

606 return await self.local.get_stats()

607

608 async def get_featured(self, limit: int = 10) -> List[AgentTemplate]:

609 """Get featured/popular templates."""

610 query = MarketSearchQuery(sort_by="downloads", limit=limit)

611 results = await self.local.list_templates(query)

612 return [r.template for r in results]

613

614 async def get_by_category(

615 self, category: TemplateCategory, limit: int = 20

616 ) -> List[AgentTemplate]:

617 """Get templates by category."""

618 query = MarketSearchQuery(category=category, limit=limit)

619 results = await self.local.list_templates(query)

620 return [r.template for r in results]

621

622

623# ---------------------------------------------------------------------------

624# Template builder

625# ---------------------------------------------------------------------------

626

627 class TemplateBuilder:

628 """Helper to build AgentTemplate objects programmatically."""

629

630 def __init__(self, name: str, author: str, version: str = "1.0.0"):

631 self._template = AgentTemplate(

632 id=hashlib.sha256(f"{author}/{name}".encode()).hexdigest()[:16],

633 name=name,

634 version=version,

635 author=author,

636 )

637

638 def description(self, text: str) -> "TemplateBuilder":

639 self._template.description = text

640 return self

641

642 def category(self, cat: TemplateCategory) -> "TemplateBuilder":

643 self._template.category = cat

644 return self

645

646 def tags(self, *tags: str) -> "TemplateBuilder":

647 self._template.tags = list(tags)

648 return self

649

650 def add_version(

651 self, version: str, files: Dict[str, str], changelog: str = ""

652 ) -> "TemplateBuilder":

653 tv = TemplateVersion(

654 version=version,

655 changelog=changelog,

656 files=files,

657 published_at=time.time(),

658 )

659 self._template.versions.append(tv)

660 return self

661

662 def add_dependency(

663 self, name: str, version_spec: str = "*", optional: bool = False

664 ) -> "TemplateBuilder":

665 self._template.requires.append(

666 TemplateDependency(name=name, version_spec=version_spec, optional=optional)

667 )

668 return self

669

670 def add_review(self, user_id: str, rating: float, comment: str = "") -> "TemplateBuilder":

671 self._template.reviews.append(

672 TemplateReview(user_id=user_id, rating=rating, comment=comment)

673 )

674 return self

675

676 def build(self) -> AgentTemplate:

677 if not self._template.description:

678 raise ValueError("Template must have a description")

679 return self._template

680

681

682# ---------------------------------------------------------------------------

683# Pre-seeded templates

684# ---------------------------------------------------------------------------

685

686 def seed_default_templates(manager: MarketplaceManager) -> None:

687 """Seed the marketplace with default templates."""

688 templates = [

689 TemplateBuilder("Conversational Agent", "AgentOS Team")

690 .description("General-purpose conversational agent with memory and tool use")

691 .category(TemplateCategory.CHAT)

692 .tags("chat", "conversation", "memory")

693 .add_version("1.0.0", {

694 "agent.yaml": "name: conversational-agent\ntype: chat\nmemory: enabled",

695 "main.py": "from agentos import Agent\n\nagent = Agent(...)",

696 })

697 .build(),

698

699 TemplateBuilder("Code Review Assistant", "AgentOS Team")

700 .description("AI-powered code reviewer with PR integration")

701 .category(TemplateCategory.CODING)

702 .tags("code", "review", "github", "pr")

703 .add_version("1.0.0", {

704 "agent.yaml": "name: code-reviewer\ntype: coding\n",

705 "review.py": "async def review_pr(pr_url): ...",

706 })

707 .build(),

708

709 TemplateBuilder("Research Analyst", "AgentOS Team")

710 .description("Multi-source research agent with deep analysis capabilities")

711 .category(TemplateCategory.RESEARCH)

712 .tags("research", "analysis", "web")

713 .add_version("1.0.0", {

714 "agent.yaml": "name: research-analyst\ntype: research\n",

715 "analyst.py": "async def deep_research(topic): ...",

716 })

717 .build(),

718

719 TemplateBuilder("Data Pipeline Agent", "AgentOS Team")

720 .description("Automated ETL and data processing pipeline")

721 .category(TemplateCategory.AUTOMATION)

722 .tags("etl", "data", "pipeline", "automation")

723 .add_version("1.0.0", {

724 "agent.yaml": "name: data-pipeline\ntype: automation\n",

725 "pipeline.py": "async def run_pipeline(config): ...",

726 })

727 .build(),

728

729 TemplateBuilder("Document Writer", "AgentOS Team")

730 .description("Professional document generation from outlines or templates")

731 .category(TemplateCategory.CREATIVE)

732 .tags("writing", "document", "report")

733 .add_version("1.0.0", {

734 "agent.yaml": "name: doc-writer\ntype: creative\n",

735 "writer.py": "async def generate_doc(outline): ...",

736 })

737 .build(),

738 ]

739

740 async def _seed():

741 for tpl in templates:

742 await manager.local.publish_template(tpl)

743

744 try:

745 asyncio.get_event_loop().run_until_complete(_seed())

746 except RuntimeError:

747 asyncio.run(_seed())

748

749

750# ---------------------------------------------------------------------------

751# Export

752# ---------------------------------------------------------------------------

753

754__all__ = [

755 # Enums

756 "TemplateCategory",

757 "TemplateStatus",

758 # Data types

759 "TemplateDependency",

760 "TemplateVersion",

761 "TemplateReview",

762 "AgentTemplate",

763 "MarketSearchQuery",

764 "MarketSearchResult",

765 # Backends

766 "MarketRegistryBackend",

767 "InMemoryMarketBackend",

768 "FileMarketBackend",

769 "RemoteMarketClient",

770 # Manager

771 "MarketplaceManager",

772 "TemplateBuilder",

773 "seed_default_templates",

774]