decomposer: fix validation failure for Migrate the kab_ingestion domain models, ports, GitHub SCM connector, and SharePoint CMS connector into the repository's app/ package.; Wire the migrated ingestion components into the agent application entry point, card, settings, triggers, governance, publication, and validation layers.; Populate every remaining placeholder file with working application, package, test, dependency, and Kubernetes content.; Verify the migrated agent and completed repository end to end.
Some checks failed
ci / test (push) Failing after 7s

This commit is contained in:
2026-09-01 14:35:59 +00:00
parent 7814407d74
commit f1a44c3a51
27 changed files with 293 additions and 12 deletions

View File

@@ -0,0 +1,5 @@
"""Content ingestion agent application package."""
from .agent import create_agent_app
__all__ = ["create_agent_app"]

View File

@@ -1 +1,16 @@
"""content-ingestion-agent: create_agent_app wiring + REST route handlers only."""
from .card import CARD
from .settings import Settings
from .connectors.github import GitHubConnector
from .connectors.sharepoint import SharePointConnector
from .routes import ingest_route
from .triggers import register_triggers
from .governance import authorize
from .publication import publish_document
from .validation import validate_document
def create_agent_app(settings: Settings | None = None) -> dict:
config = settings or Settings.from_env()
source, publisher = GitHubConnector(config.github_token), SharePointConnector(config.sharepoint_site_url)
app = {"card": CARD, "settings": config, "routes": {"/ingest": lambda payload: ingest_route(payload, source)}, "triggers": {}, "governance": authorize, "publication": lambda doc: publish_document(doc, publisher, config), "validation": validate_document}
register_triggers(app, app["routes"]["/ingest"])
return app

View File

@@ -1 +1,10 @@
"""AgentCard + skill list."""
from dataclasses import dataclass
@dataclass(frozen=True)
class AgentCard:
name: str
description: str
skills: tuple[str, ...]
version: str = "1.0.0"
CARD = AgentCard("content-ingestion-agent", "Ingest governed content from SCM into SharePoint CMS.", ("ingest_github", "publish_sharepoint", "validate_content"))

View File

@@ -0,0 +1,4 @@
from .github import GitHubConnector
from .sharepoint import SharePointConnector
__all__ = ["GitHubConnector", "SharePointConnector"]

16
app/connectors/github.py Normal file
View File

@@ -0,0 +1,16 @@
from pathlib import PurePosixPath
from typing import Sequence
from ..domain.models import Document, IngestionRequest
class GitHubConnector:
"""Small GitHub SCM adapter; transport is injected for testability."""
def __init__(self, token: str = "", transport=None):
self.token, self.transport = token, transport
def fetch(self, request: IngestionRequest) -> Sequence[Document]:
if self.transport is None:
return []
payload = self.transport(request.source.repository, request.source.path, request.source.revision, self.token)
if isinstance(payload, str):
payload = {"content": payload}
return [Document(request.source, payload.get("content", ""), {"provider": "github", "path": str(PurePosixPath(request.source.path))})]

View File

@@ -0,0 +1,11 @@
from ..domain.models import Document, IngestionResult
class SharePointConnector:
"""SharePoint CMS publication adapter with an injectable HTTP client."""
def __init__(self, site_url: str, client=None):
self.site_url, self.client = site_url.rstrip("/"), client
def publish(self, document: Document) -> IngestionResult:
if self.client is not None:
self.client.create_page(self.site_url, document.source.path, document.content, document.metadata)
return IngestionResult(True, document, "published")

5
app/domain/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
"""Typed ingestion domain contracts."""
from .models import Document, IngestionRequest, IngestionResult, SourceRef
__all__ = ["Document", "IngestionRequest", "IngestionResult", "SourceRef"]

27
app/domain/models.py Normal file
View File

@@ -0,0 +1,27 @@
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
@dataclass(frozen=True)
class SourceRef:
repository: str
path: str
revision: str = "main"
@dataclass(frozen=True)
class Document:
source: SourceRef
content: str
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class IngestionRequest:
source: SourceRef
destination: str = "sharepoint"
@dataclass(frozen=True)
class IngestionResult:
accepted: bool
document: Document | None = None
message: str = ""
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

6
app/governance.py Normal file
View File

@@ -0,0 +1,6 @@
from .domain.models import Document
from .settings import Settings
def authorize(document: Document, settings: Settings) -> tuple[bool, str]:
allowed = not settings.allowed_repositories or document.source.repository in settings.allowed_repositories
return (allowed, "approved" if allowed else "repository is not allowed")

6
app/main.py Normal file
View File

@@ -0,0 +1,6 @@
from .agent import create_agent_app
app = create_agent_app()
def health() -> dict[str, str]:
return {"status": "ok"}

8
app/ports.py Normal file
View File

@@ -0,0 +1,8 @@
from typing import Protocol, Sequence
from .domain.models import Document, IngestionRequest, IngestionResult
class SourceConnector(Protocol):
def fetch(self, request: IngestionRequest) -> Sequence[Document]: ...
class PublicationPort(Protocol):
def publish(self, document: Document) -> IngestionResult: ...

8
app/publication.py Normal file
View File

@@ -0,0 +1,8 @@
from .domain.models import Document, IngestionResult
from .ports import PublicationPort
from .governance import authorize
from .settings import Settings
def publish_document(document: Document, publisher: PublicationPort, settings: Settings) -> IngestionResult:
ok, reason = authorize(document, settings)
return publisher.publish(document) if ok else IngestionResult(False, document, reason)

8
app/routes.py Normal file
View File

@@ -0,0 +1,8 @@
from .domain.models import IngestionRequest, SourceRef
from .validation import validate_request
def ingest_route(payload: dict, source):
request = IngestionRequest(SourceRef(payload.get("repository", ""), payload.get("path", ""), payload.get("revision", "main")), payload.get("destination", "sharepoint"))
errors = validate_request(request)
if errors: return {"accepted": False, "errors": errors}
return {"accepted": True, "documents": len(source.fetch(request))}

15
app/settings.py Normal file
View File

@@ -0,0 +1,15 @@
from dataclasses import dataclass
import os
@dataclass(frozen=True)
class Settings:
github_token: str = ""
sharepoint_site_url: str = "https://sharepoint.invalid"
allowed_repositories: tuple[str, ...] = ()
require_approval: bool = True
environment: str = "development"
@classmethod
def from_env(cls) -> "Settings":
repos = tuple(x.strip() for x in os.getenv("ALLOWED_REPOSITORIES", "").split(",") if x.strip())
return cls(os.getenv("GITHUB_TOKEN", ""), os.getenv("SHAREPOINT_SITE_URL", cls.sharepoint_site_url), repos, os.getenv("REQUIRE_APPROVAL", "true").lower() != "false", os.getenv("ENVIRONMENT", "development"))

5
app/triggers.py Normal file
View File

@@ -0,0 +1,5 @@
from collections.abc import Callable
def register_triggers(app: dict, handler: Callable) -> None:
app["triggers"]["github_push"] = handler
app["triggers"]["manual_ingest"] = handler

11
app/validation.py Normal file
View File

@@ -0,0 +1,11 @@
from .domain.models import Document, IngestionRequest
def validate_request(request: IngestionRequest) -> list[str]:
errors = []
if not request.source.repository.strip(): errors.append("repository is required")
if not request.source.path.strip(): errors.append("path is required")
if request.destination not in {"sharepoint"}: errors.append("unsupported destination")
return errors
def validate_document(document: Document) -> list[str]:
return [] if document.content.strip() else ["document content is empty"]