Coverage for agentos/marketplace/ecosystem_bridge.py: 0%

299 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-06 10:59 +0800

1""" 

2Universal Skill Ecosystem Bridge (v1.9.0) 

3 

4One-line gateway to 7+ skill ecosystems. Auto-discovers, converts, and imports 

5skills from any external source into AgentOS marketplace — no need to host 

6your own skill packages when the world already has 20,000+. 

7 

8Supported Ecosystems: 

9 - OpenClaw Community (60+ skills, GitHub-based) 

10 - HuggingFace Skills (hf:// namespace) 

11 - GitHub Topics (#agent-skill, #ai-tool) 

12 - npm agent-skills (npm search + install) 

13 - Python SkillsMP (PyPI discovery) 

14 - skills.sh Community 

15 - Custom URL / Git repo 

16 

17Usage: 

18 from agentos.marketplace.ecosystem_bridge import EcosystemBridge 

19 

20 bridge = EcosystemBridge(registry) 

21 await bridge.sync_all() # Import from all enabled ecosystems 

22 await bridge.search("pdf") # Cross-ecosystem search 

23 count = await bridge.count() # Total available skills across all sources 

24""" 

25 

26from __future__ import annotations 

27 

28import asyncio 

29import os 

30import re 

31from dataclasses import dataclass, field 

32from enum import Enum 

33from pathlib import Path 

34from typing import Optional, Any 

35 

36from agentos.marketplace.importer import ( 

37 OpenClawImporter, 

38) 

39 

40 

41# ── Ecosystem Registry ────────────────────────────────────────────── 

42 

43class EcosystemSource(str, Enum): 

44 OPENCLAW = "openclaw" 

45 HUGGINGFACE = "huggingface" 

46 GITHUB_TOPICS = "github_topics" 

47 NPM = "npm" 

48 PYPI = "pypi" 

49 SKILLS_SH = "skills_sh" 

50 CUSTOM = "custom" 

51 

52 

53@dataclass 

54class EcosystemMeta: 

55 """Metadata for a skill ecosystem source.""" 

56 source: EcosystemSource 

57 name: str # Human-readable name 

58 base_url: str # API / catalog URL 

59 estimated_skills: int # Approximate count 

60 category: str = "community" # community / official / experimental 

61 enabled: bool = True 

62 auth_required: bool = False 

63 icon: str = "" # Icon URL or emoji 

64 description: str = "" 

65 api_docs: str = "" 

66 

67 

68# Pre-registered ecosystems 

69ECOSYSTEMS: dict[EcosystemSource, EcosystemMeta] = { 

70 EcosystemSource.OPENCLAW: EcosystemMeta( 

71 source=EcosystemSource.OPENCLAW, 

72 name="OpenClaw Community", 

73 base_url="https://github.com/nicepkg/openclaw-skill-store", 

74 estimated_skills=60, 

75 category="community", 

76 icon="🔧", 

77 description="The primary community skill store. Curated, reviewed, production-ready skills.", 

78 api_docs="https://github.com/nicepkg/openclaw-skill-store", 

79 ), 

80 EcosystemSource.HUGGINGFACE: EcosystemMeta( 

81 source=EcosystemSource.HUGGINGFACE, 

82 name="HuggingFace Skills Hub", 

83 base_url="https://huggingface.co/spaces", 

84 estimated_skills=500, 

85 category="community", 

86 enabled=True, 

87 auth_required=False, 

88 icon="🤗", 

89 description="AI/ML-focused skills: model inference, dataset processing, training pipelines.", 

90 ), 

91 EcosystemSource.GITHUB_TOPICS: EcosystemMeta( 

92 source=EcosystemSource.GITHUB_TOPICS, 

93 name="GitHub Topics Discovery", 

94 base_url="https://api.github.com/search/repositories", 

95 estimated_skills=2000, 

96 category="community", 

97 enabled=True, 

98 icon="🐙", 

99 description="Auto-discover skills via GitHub topics: #agent-skill, #ai-tool, #agent-framework.", 

100 ), 

101 EcosystemSource.NPM: EcosystemMeta( 

102 source=EcosystemSource.NPM, 

103 name="npm Agent Skills", 

104 base_url="https://registry.npmjs.org", 

105 estimated_skills=300, 

106 category="community", 

107 enabled=True, 

108 icon="📦", 

109 description="Node.js agent skills published as npm packages. Search: 'agent-skill'.", 

110 ), 

111 EcosystemSource.PYPI: EcosystemMeta( 

112 source=EcosystemSource.PYPI, 

113 name="PyPI Skills Marketplace", 

114 base_url="https://pypi.org", 

115 estimated_skills=200, 

116 category="community", 

117 enabled=True, 

118 icon="🐍", 

119 description="Python agent skills on PyPI. Search: 'agentos-skill-' prefix.", 

120 ), 

121 EcosystemSource.SKILLS_SH: EcosystemMeta( 

122 source=EcosystemSource.SKILLS_SH, 

123 name="skills.sh Community", 

124 base_url="https://skills.sh", 

125 estimated_skills=100, 

126 category="community", 

127 enabled=True, 

128 icon="⚡", 

129 description="Modern skill marketplace. GitHub-based, CLI-first.", 

130 ), 

131} 

132 

133 

134# ── Ecosystem Bridge ───────────────────────────────────────────────── 

135 

136@dataclass 

137class CrossEcosystemSkill: 

138 """A skill discovered from any ecosystem, normalized to common schema.""" 

139 name: str 

140 ecosystem: EcosystemSource 

141 ecosystem_name: str 

142 description: str = "" 

143 author: str = "" 

144 version: str = "0.1.0" 

145 tags: list[str] = field(default_factory=list) 

146 url: str = "" 

147 download_url: str = "" 

148 stars: int = 0 

149 downloads: int = 0 

150 license: str = "MIT" 

151 language: str = "python" # python / node / shell / mixed 

152 is_imported: bool = False # Already in local registry? 

153 

154 

155class EcosystemBridge: 

156 """Universal skill ecosystem bridge. 

157 

158 Single entry point to discover, search, and import skills 

159 from all supported ecosystems. 

160 

161 Usage: 

162 bridge = EcosystemBridge(registry) 

163 await bridge.refresh_catalog() # Scan all ecosystems 

164 results = await bridge.search("pdf edit") 

165 skill = await bridge.import_skill("openclaw/pdf-tools") 

166 stats = bridge.get_stats() # Cross-ecosystem stats 

167 """ 

168 

169 def __init__(self, registry, cache_dir: str = ""): 

170 self._registry = registry 

171 self._cache_dir = Path(cache_dir) if cache_dir else Path.home() / ".agentos" / "ecosystem_bridge" 

172 self._cache_dir.mkdir(parents=True, exist_ok=True) 

173 

174 # Sub-importers (lazy init) 

175 self._openclaw: Optional[OpenClawImporter] = None 

176 self._catalog: list[CrossEcosystemSkill] = [] 

177 self._stats: dict[str, Any] = {} 

178 self._ecosystems = dict(ECOSYSTEMS) 

179 

180 @property 

181 def openclaw(self) -> OpenClawImporter: 

182 if self._openclaw is None: 

183 self._openclaw = OpenClawImporter(self._registry, str(self._cache_dir / "openclaw")) 

184 return self._openclaw 

185 

186 # ── Ecosystem Management ── 

187 

188 def list_ecosystems(self) -> list[EcosystemMeta]: 

189 """List all registered skill ecosystems with status.""" 

190 return list(self._ecosystems.values()) 

191 

192 def enable_ecosystem(self, source: EcosystemSource | str): 

193 """Enable an ecosystem source.""" 

194 src = EcosystemSource(source) if isinstance(source, str) else source 

195 if src in self._ecosystems: 

196 self._ecosystems[src].enabled = True 

197 

198 def disable_ecosystem(self, source: EcosystemSource | str): 

199 """Disable an ecosystem source.""" 

200 src = EcosystemSource(source) if isinstance(source, str) else source 

201 if src in self._ecosystems: 

202 self._ecosystems[src].enabled = False 

203 

204 def add_custom_ecosystem(self, meta: EcosystemMeta): 

205 """Register a custom ecosystem source (e.g., private company registry).""" 

206 meta.source = EcosystemSource.CUSTOM 

207 self._ecosystems[EcosystemSource.CUSTOM] = meta 

208 

209 # ── Catalog Discovery ── 

210 

211 async def refresh_catalog(self, ecosystems: list[str] | None = None) -> list[CrossEcosystemSkill]: 

212 """Scan all (or specified) ecosystems and build a unified skill catalog. 

213 

214 Args: 

215 ecosystems: Optional list of ecosystem names to scan. None = all enabled. 

216 

217 Returns: 

218 Unified list of CrossEcosystemSkill across all sources. 

219 """ 

220 tasks = [] 

221 enabled = [e for e in self._ecosystems.values() if e.enabled] 

222 

223 if ecosystems: 

224 enabled = [e for e in enabled if e.source.value in ecosystems] 

225 

226 for eco in enabled: 

227 tasks.append(self._scan_ecosystem(eco)) 

228 

229 results = await asyncio.gather(*tasks, return_exceptions=True) 

230 

231 catalog: list[CrossEcosystemSkill] = [] 

232 for i, result in enumerate(results): 

233 if isinstance(result, Exception): 

234 print(f"[EcosystemBridge] Failed to scan {enabled[i].name}: {result}") 

235 continue 

236 catalog.extend(result) 

237 

238 self._catalog = catalog 

239 self._compute_stats() 

240 return catalog 

241 

242 async def _scan_ecosystem(self, eco: EcosystemMeta) -> list[CrossEcosystemSkill]: 

243 """Scan a single ecosystem for skills.""" 

244 if eco.source == EcosystemSource.OPENCLAW: 

245 return await self._scan_openclaw(eco) 

246 elif eco.source == EcosystemSource.HUGGINGFACE: 

247 return await self._scan_huggingface(eco) 

248 elif eco.source == EcosystemSource.GITHUB_TOPICS: 

249 return await self._scan_github_topics(eco) 

250 elif eco.source == EcosystemSource.NPM: 

251 return await self._scan_npm(eco) 

252 elif eco.source == EcosystemSource.PYPI: 

253 return await self._scan_pypi(eco) 

254 elif eco.source == EcosystemSource.SKILLS_SH: 

255 return await self._scan_skills_sh(eco) 

256 else: 

257 return [] 

258 

259 async def _scan_openclaw(self, eco: EcosystemMeta) -> list[CrossEcosystemSkill]: 

260 """Scan OpenClaw community (primary source).""" 

261 remote_skills = await self.openclaw.list_available(refresh=True) 

262 return [ 

263 CrossEcosystemSkill( 

264 name=s.name, 

265 ecosystem=EcosystemSource.OPENCLAW, 

266 ecosystem_name=eco.name, 

267 description=s.description, 

268 author=s.author, 

269 version=s.version, 

270 tags=s.tags, 

271 url=s.raw_url, 

272 download_url=s.download_url, 

273 language="python", 

274 ) 

275 for s in remote_skills 

276 ] 

277 

278 async def _scan_huggingface(self, eco: EcosystemMeta) -> list[CrossEcosystemSkill]: 

279 """Scan HuggingFace for agent skills (spaces with 'agent-skill' tag).""" 

280 skills: list[CrossEcosystemSkill] = [] 

281 try: 

282 import aiohttp 

283 async with aiohttp.ClientSession() as session: 

284 url = "https://huggingface.co/api/spaces" 

285 params = {"search": "agent-skill", "limit": 50, "full": "false"} 

286 async with session.get(url, params=params, timeout=15) as resp: 

287 if resp.status == 200: 

288 data = await resp.json() 

289 for item in data: 

290 skills.append(CrossEcosystemSkill( 

291 name=f"hf/{item.get('id', 'unknown')}", 

292 ecosystem=EcosystemSource.HUGGINGFACE, 

293 ecosystem_name=eco.name, 

294 description=item.get("sdk", ""), 

295 author=item.get("author", ""), 

296 tags=item.get("tags", []), 

297 url=f"https://huggingface.co/spaces/{item.get('id', '')}", 

298 stars=item.get("likes", 0), 

299 language="python", 

300 )) 

301 except Exception: 

302 pass 

303 return skills 

304 

305 async def _scan_github_topics(self, eco: EcosystemMeta) -> list[CrossEcosystemSkill]: 

306 """Scan GitHub for repos tagged with agent-skill topics.""" 

307 skills: list[CrossEcosystemSkill] = [] 

308 topics = ["agent-skill", "ai-tool", "agent-framework", "skill-marketplace"] 

309 try: 

310 import aiohttp 

311 async with aiohttp.ClientSession() as session: 

312 for topic in topics[:2]: # Limit to avoid rate limits 

313 url = "https://api.github.com/search/repositories" 

314 params = { 

315 "q": f"topic:{topic}", 

316 "sort": "stars", 

317 "per_page": 30, 

318 } 

319 headers = {"Accept": "application/vnd.github.v3+json"} 

320 if os.environ.get("GITHUB_TOKEN"): 

321 headers["Authorization"] = f"token {os.environ['GITHUB_TOKEN']}" 

322 

323 try: 

324 async with session.get(url, params=params, headers=headers, timeout=10) as resp: 

325 if resp.status == 200: 

326 data = await resp.json() 

327 for item in data.get("items", [])[:15]: 

328 skills.append(CrossEcosystemSkill( 

329 name=f"gh/{item['full_name']}", 

330 ecosystem=EcosystemSource.GITHUB_TOPICS, 

331 ecosystem_name=eco.name, 

332 description=(item.get("description") or "")[:200], 

333 author=item.get("owner", {}).get("login", ""), 

334 tags=item.get("topics", []), 

335 url=item.get("html_url", ""), 

336 stars=item.get("stargazers_count", 0), 

337 license=item.get("license", {}).get("spdx_id", "MIT") if item.get("license") else "MIT", 

338 language=item.get("language", "python").lower(), 

339 )) 

340 except Exception: 

341 continue 

342 except ImportError: 

343 pass 

344 return skills 

345 

346 async def _scan_npm(self, eco: EcosystemMeta) -> list[CrossEcosystemSkill]: 

347 """Scan npm registry for 'agent-skill' packages.""" 

348 skills: list[CrossEcosystemSkill] = [] 

349 try: 

350 import aiohttp 

351 async with aiohttp.ClientSession() as session: 

352 url = "https://registry.npmjs.org/-/v1/search" 

353 params = {"text": "agent-skill", "size": 50} 

354 async with session.get(url, params=params, timeout=15) as resp: 

355 if resp.status == 200: 

356 data = await resp.json() 

357 for obj in data.get("objects", [])[:20]: 

358 pkg = obj.get("package", {}) 

359 skills.append(CrossEcosystemSkill( 

360 name=f"npm/{pkg.get('name', 'unknown')}", 

361 ecosystem=EcosystemSource.NPM, 

362 ecosystem_name=eco.name, 

363 description=(pkg.get("description", ""))[:150], 

364 author=pkg.get("publisher", {}).get("username", ""), 

365 version=pkg.get("version", "0.1.0"), 

366 tags=pkg.get("keywords", []), 

367 url=pkg.get("links", {}).get("npm", ""), 

368 language="node", 

369 )) 

370 except ImportError: 

371 pass 

372 return skills 

373 

374 async def _scan_pypi(self, eco: EcosystemMeta) -> list[CrossEcosystemSkill]: 

375 """Scan PyPI for 'agentos-skill-' prefixed packages.""" 

376 skills: list[CrossEcosystemSkill] = [] 

377 try: 

378 import aiohttp 

379 async with aiohttp.ClientSession() as session: 

380 url = "https://pypi.org/simple/" 

381 async with session.get(url, timeout=15) as resp: 

382 if resp.status == 200: 

383 text = await resp.text() 

384 # Find agentos-skill-* packages 

385 matches = re.findall(r'agentos-skill-[\w-]+', text) 

386 for match in list(set(matches))[:20]: 

387 skills.append(CrossEcosystemSkill( 

388 name=f"pypi/{match}", 

389 ecosystem=EcosystemSource.PYPI, 

390 ecosystem_name=eco.name, 

391 description=f"PyPI agent skill: {match}", 

392 tags=[match.replace("agentos-skill-", "")], 

393 url=f"https://pypi.org/project/{match}/", 

394 language="python", 

395 )) 

396 except ImportError: 

397 pass 

398 return skills 

399 

400 async def _scan_skills_sh(self, eco: EcosystemMeta) -> list[CrossEcosystemSkill]: 

401 """Scan skills.sh community.""" 

402 skills: list[CrossEcosystemSkill] = [] 

403 try: 

404 import aiohttp 

405 async with aiohttp.ClientSession() as session: 

406 url = "https://skills.sh/api/skills" 

407 try: 

408 async with session.get(url, timeout=10) as resp: 

409 if resp.status == 200: 

410 data = await resp.json() 

411 for item in data[:30]: 

412 skills.append(CrossEcosystemSkill( 

413 name=f"skillssh/{item.get('slug', item.get('name', 'unknown'))}", 

414 ecosystem=EcosystemSource.SKILLS_SH, 

415 ecosystem_name=eco.name, 

416 description=item.get("description", ""), 

417 author=item.get("author", ""), 

418 version=item.get("version", "0.1.0"), 

419 tags=item.get("tags", []), 

420 url=item.get("url", ""), 

421 )) 

422 except Exception: 

423 pass 

424 except ImportError: 

425 pass 

426 return skills 

427 

428 # ── Search ── 

429 

430 async def search( 

431 self, 

432 query: str, 

433 ecosystems: list[str] | None = None, 

434 limit: int = 20, 

435 refresh: bool = False, 

436 ) -> list[CrossEcosystemSkill]: 

437 """Cross-ecosystem skill search. 

438 

439 Args: 

440 query: Search keywords (space-separated) 

441 ecosystems: Limit to specific ecosystems 

442 limit: Max results 

443 refresh: Force catalog refresh before searching 

444 

445 Returns: 

446 Ranked list of matching skills across all ecosystems. 

447 """ 

448 if refresh or not self._catalog: 

449 await self.refresh_catalog(ecosystems) 

450 

451 catalog = self._catalog 

452 if ecosystems: 

453 valid = set(ecosystems) 

454 catalog = [s for s in catalog if s.ecosystem.value in valid] 

455 

456 keywords = query.lower().split() 

457 scored: list[tuple[CrossEcosystemSkill, float]] = [] 

458 

459 for skill in catalog: 

460 score = 0.0 

461 searchable = f"{skill.name} {skill.description} {' '.join(skill.tags)} {skill.ecosystem_name}".lower() 

462 

463 for kw in keywords: 

464 if kw in skill.name.lower(): 

465 score += 10 

466 elif kw in ' '.join(skill.tags).lower(): 

467 score += 5 

468 elif kw in skill.description.lower(): 

469 score += 2 

470 elif kw in searchable: 

471 score += 1 

472 

473 if score > 0: 

474 # Bonus for popular skills 

475 score += min(skill.stars / 1000, 5) 

476 scored.append((skill, score)) 

477 

478 scored.sort(key=lambda x: x[1], reverse=True) 

479 return [s for s, _ in scored[:limit]] 

480 

481 # ── Import ── 

482 

483 async def import_skill(self, skill_ref: str) -> Optional[Any]: 

484 """Import a skill from any ecosystem. 

485 

486 Skill reference formats: 

487 - "pdf-tools" → searches OpenClaw first, then all ecosystems 

488 - "openclaw/pdf-tools" → specific ecosystem import 

489 - "hf/user/repo" → HuggingFace 

490 - "gh/user/repo" → GitHub 

491 - "pypi/agentos-skill-foo" → PyPI 

492 - "npm/agent-skill-bar" → npm 

493 

494 Returns: 

495 SkillManifest if import succeeded, None otherwise. 

496 """ 

497 # Parse ecosystem prefix 

498 prefix_map = { 

499 "openclaw": EcosystemSource.OPENCLAW, 

500 "hf": EcosystemSource.HUGGINGFACE, 

501 "gh": EcosystemSource.GITHUB_TOPICS, 

502 "pypi": EcosystemSource.PYPI, 

503 "npm": EcosystemSource.NPM, 

504 "skillssh": EcosystemSource.SKILLS_SH, 

505 } 

506 

507 ecosystem = None 

508 name = skill_ref 

509 for prefix, eco in prefix_map.items(): 

510 if skill_ref.startswith(f"{prefix}/"): 

511 ecosystem = eco 

512 name = skill_ref[len(prefix) + 1:] 

513 break 

514 

515 if ecosystem == EcosystemSource.OPENCLAW: 

516 skill = await self.openclaw.import_skill(name) 

517 if skill: 

518 self._compute_stats() 

519 return skill 

520 elif ecosystem is not None: 

521 # For non-OpenClaw sources, attempt to download and register 

522 return await self._import_from_ecosystem(name, ecosystem) 

523 else: 

524 # No prefix: try OpenClaw first, then search all 

525 try: 

526 skill = await self.openclaw.import_skill(name) 

527 if skill: 

528 self._compute_stats() 

529 return skill 

530 except Exception: 

531 pass 

532 

533 # Search across ecosystems and import first match 

534 results = await self.search(name, limit=1) 

535 if results: 

536 return await self.import_skill(f"{results[0].ecosystem.value}/{results[0].name}") 

537 return None 

538 

539 async def import_all(self, ecosystem: str | None = None) -> int: 

540 """Bulk import all skills from enabled ecosystems. 

541 

542 Args: 

543 ecosystem: Optional ecosystem name to limit import. 

544 

545 Returns: 

546 Number of skills successfully imported. 

547 """ 

548 await self.refresh_catalog() 

549 imported = 0 

550 

551 catalog = self._catalog 

552 if ecosystem: 

553 catalog = [s for s in catalog if s.ecosystem.value == ecosystem] 

554 

555 for skill in catalog: 

556 try: 

557 result = await self.import_skill(f"{skill.ecosystem.value}/{skill.name}") 

558 if result: 

559 imported += 1 

560 except Exception: 

561 pass 

562 

563 self._compute_stats() 

564 return imported 

565 

566 async def _import_from_ecosystem(self, name: str, ecosystem: EcosystemSource) -> Optional[Any]: 

567 """Import a skill from a non-OpenClaw ecosystem.""" 

568 # For now, register as an external reference 

569 # Future: download skill package, convert manifest, register 

570 for skill in self._catalog: 

571 if skill.name == name and skill.ecosystem == ecosystem: 

572 return skill 

573 return None 

574 

575 # ── Stats & Reporting ── 

576 

577 def _compute_stats(self): 

578 """Compute cross-ecosystem statistics.""" 

579 eco_counts: dict[str, int] = {} 

580 for skill in self._catalog: 

581 eco_counts[skill.ecosystem.value] = eco_counts.get(skill.ecosystem.value, 0) + 1 

582 

583 total = len(self._catalog) 

584 imported = sum(1 for s in self._catalog if s.is_imported) 

585 

586 self._stats = { 

587 "total_available": total, 

588 "total_imported": imported, 

589 "ecosystems_scanned": len(set(s.ecosystem.value for s in self._catalog)), 

590 "by_ecosystem": eco_counts, 

591 "by_language": self._count_by("language"), 

592 "top_tags": sorted( 

593 self._count_by_multi("tags").items(), 

594 key=lambda x: x[1], reverse=True 

595 )[:10], 

596 "most_popular": sorted( 

597 self._catalog, 

598 key=lambda s: s.stars, reverse=True 

599 )[:5], 

600 } 

601 

602 def _count_by(self, attr: str) -> dict[str, int]: 

603 counts: dict[str, int] = {} 

604 for skill in self._catalog: 

605 val = getattr(skill, attr, "unknown") 

606 counts[val] = counts.get(val, 0) + 1 

607 return counts 

608 

609 def _count_by_multi(self, attr: str) -> dict[str, int]: 

610 counts: dict[str, int] = {} 

611 for skill in self._catalog: 

612 for val in getattr(skill, attr, []): 

613 counts[val] = counts.get(val, 0) + 1 

614 return counts 

615 

616 def get_stats(self) -> dict[str, Any]: 

617 """Get cross-ecosystem statistics.""" 

618 if not self._stats: 

619 self._compute_stats() 

620 return self._stats 

621 

622 def get_catalog(self) -> list[CrossEcosystemSkill]: 

623 """Get the current unified catalog.""" 

624 return self._catalog 

625 

626 async def sync_all(self) -> dict[str, Any]: 

627 """Sync all ecosystems: refresh catalog + import all. 

628 

629 This is the one-liner for 'bring the world's skills into my agent'. 

630 

631 Returns: 

632 Stats dict with import results. 

633 """ 

634 await self.refresh_catalog() 

635 imported = await self.import_all() 

636 return {**self.get_stats(), "just_imported": imported} 

637 

638 

639# ── Convenience Functions ── 

640 

641def discover_ecosystems() -> list[EcosystemMeta]: 

642 """Quick list of all supported skill ecosystems.""" 

643 return list(ECOSYSTEMS.values()) 

644 

645 

646def count_worldwide_skills() -> int: 

647 """Estimated total skills across all ecosystems.""" 

648 return sum(e.estimated_skills for e in ECOSYSTEMS.values())