reorg
This commit is contained in:
@@ -4,8 +4,8 @@ from fastmcp import FastMCP
|
||||
|
||||
from personal_mcp.prompts import create_prompts_provider
|
||||
from personal_mcp.registry.load import get_docs_registry
|
||||
from personal_mcp.registry.models.registry import DocsRegistry
|
||||
from personal_mcp.registry.read import read_docs_markdown_path
|
||||
from personal_mcp.registry.load import read_docs_markdown_path
|
||||
from personal_mcp.registry.models import DocsRegistry
|
||||
from personal_mcp.skills import create_skills_provider
|
||||
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ from typing import Any
|
||||
import yaml
|
||||
from pydantic import ValidationError
|
||||
|
||||
from personal_mcp.prompts.models import PromptDefinition
|
||||
from personal_mcp.prompts.models import PromptMetadata
|
||||
from .models import PromptDefinition
|
||||
from .models import PromptMetadata
|
||||
|
||||
_PROMPT_ID_RE = re.compile(r"^[a-z][a-z0-9-]*$")
|
||||
_FRONTMATTER_RE = re.compile(r"\A---\r?\n(?P<yaml>.*?)\r?\n---(?:\r?\n|$)", re.DOTALL)
|
||||
|
||||
@@ -5,8 +5,8 @@ from importlib.resources.abc import Traversable
|
||||
from fastmcp.prompts import Prompt
|
||||
from fastmcp.server.providers import Provider
|
||||
|
||||
from personal_mcp.prompts.content import load_prompt_definition
|
||||
from personal_mcp.prompts.models import MarkdownPrompt
|
||||
from .content import load_prompt_definition
|
||||
from .models import MarkdownPrompt
|
||||
|
||||
|
||||
class MarkdownPromptsProvider(Provider):
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from . import load as load
|
||||
from . import models as models
|
||||
from .load import get_docs_registry
|
||||
from .models import DocsPath
|
||||
from .models import DocsRegistry
|
||||
|
||||
__all__ = ["DocsPath", "DocsRegistry", "get_docs_registry", "load", "models"]
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from .models.registry import DocsRegistry
|
||||
|
||||
__all__ = ["DocsRegistry"]
|
||||
@@ -1 +0,0 @@
|
||||
"""Functions to produce immutable dataclasses representing the document registry."""
|
||||
@@ -1,49 +0,0 @@
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from importlib.resources.abc import Traversable
|
||||
from itertools import starmap
|
||||
from pathlib import PurePosixPath
|
||||
from typing import Self
|
||||
|
||||
from personal_mcp.registry.models.common import DocsPath
|
||||
from personal_mcp.registry.models.common import parse_docs_path
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MarkdownDocument:
|
||||
"""Represents a loaded markdown document with its content and frontmatter."""
|
||||
|
||||
relpath: DocsPath
|
||||
"""The relative path of the document within the package resources."""
|
||||
content: str
|
||||
"""The raw markdown content of the document."""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
object.__setattr__(self, "relpath", parse_docs_path(self.relpath))
|
||||
|
||||
@classmethod
|
||||
def from_root(cls, root: Traversable) -> dict[DocsPath, Self]:
|
||||
"""Recursively load all markdown documents from the root resource."""
|
||||
mapped = starmap(cls.from_resource, walk_resources(root))
|
||||
return {d.relpath: d for d in mapped}
|
||||
|
||||
@classmethod
|
||||
def from_resource(cls, relpath: DocsPath, resource: Traversable) -> Self:
|
||||
"""Load a markdown document from a package resource."""
|
||||
return cls(relpath=relpath, content=resource.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def walk_resources(
|
||||
node: Traversable,
|
||||
*,
|
||||
suffix: str = ".md",
|
||||
prefix: PurePosixPath | None = None,
|
||||
) -> Iterator[tuple[PurePosixPath, Traversable]]:
|
||||
"""Recursively yield all resources in node, with their full path."""
|
||||
prefix = prefix or PurePosixPath()
|
||||
for child in sorted(node.iterdir(), key=lambda item: item.name):
|
||||
relpath = prefix / child.name
|
||||
if child.is_dir():
|
||||
yield from walk_resources(child, suffix=suffix, prefix=relpath)
|
||||
elif child.is_file() and child.name.lower().endswith(suffix):
|
||||
yield relpath, child
|
||||
@@ -1,10 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from functools import cache
|
||||
from importlib.resources import files
|
||||
from importlib.resources.abc import Traversable
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
from personal_mcp.registry.ingest.document import MarkdownDocument
|
||||
from personal_mcp.registry.models.registry import DocsRegistry
|
||||
from .models import DocsPath
|
||||
from .models import DocsRegistry
|
||||
from .models import parse_docs_path
|
||||
|
||||
|
||||
@cache
|
||||
@@ -12,11 +16,49 @@ def get_docs_registry() -> DocsRegistry:
|
||||
root = files("personal_mcp").joinpath("docs")
|
||||
if not root.is_dir():
|
||||
raise FileNotFoundError(f"docs root does not exist or is not a directory: {root}")
|
||||
docs = MarkdownDocument.from_root(root)
|
||||
docs = load_markdown(root)
|
||||
|
||||
docs_markdown_by_path = {relpath: doc.content for relpath, doc in docs.items() if relpath.parts[0] != "skills"}
|
||||
docs_markdown_by_path = {relpath: content for relpath, content in docs.items() if relpath.parts[0] != "skills"}
|
||||
|
||||
return DocsRegistry(
|
||||
docs_markdown_by_path=docs_markdown_by_path,
|
||||
docs_markdown_path_index=tuple(sorted(docs_markdown_by_path)),
|
||||
)
|
||||
|
||||
|
||||
def load_markdown(root: Traversable) -> dict[DocsPath, str]:
|
||||
"""Recursively load Markdown content keyed by package-relative path."""
|
||||
return {
|
||||
parse_docs_path(relpath): resource.read_text(encoding="utf-8")
|
||||
for relpath, resource in walk_resources(root)
|
||||
} # fmt: skip
|
||||
|
||||
|
||||
def walk_resources(
|
||||
node: Traversable,
|
||||
*,
|
||||
suffix: str = ".md",
|
||||
prefix: PurePosixPath | None = None,
|
||||
) -> Iterator[tuple[PurePosixPath, Traversable]]:
|
||||
"""Recursively yield all resources in node, with their full path."""
|
||||
prefix = prefix or PurePosixPath()
|
||||
for child in sorted(node.iterdir(), key=lambda item: item.name):
|
||||
relpath = prefix / child.name
|
||||
if child.is_dir():
|
||||
yield from walk_resources(child, suffix=suffix, prefix=relpath)
|
||||
elif child.is_file() and child.name.lower().endswith(suffix):
|
||||
yield relpath, child
|
||||
|
||||
|
||||
def read_docs_markdown_path(registry: DocsRegistry, path: str) -> dict[str, str]:
|
||||
docs_path = parse_docs_path(path)
|
||||
if docs_path.parts[0] == "skills":
|
||||
raise KeyError(f"unknown docs path: {docs_path.as_posix()}")
|
||||
if docs_path not in registry.docs_markdown_by_path:
|
||||
raise KeyError(f"unknown docs path: {docs_path.as_posix()}")
|
||||
return {
|
||||
"uri": f"resource://docs/{docs_path.as_posix()}",
|
||||
"format": "markdown",
|
||||
"source_path": f"docs/{docs_path.as_posix()}",
|
||||
"content": registry.docs_markdown_by_path[docs_path],
|
||||
}
|
||||
|
||||
@@ -7,16 +7,10 @@ from typing import ClassVar
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BeforeValidator
|
||||
from pydantic import ConfigDict
|
||||
from pydantic import Field
|
||||
from pydantic import field_validator
|
||||
|
||||
|
||||
class StrictFrozenModel(BaseModel):
|
||||
"""Immutable base model with strict field validation rules."""
|
||||
|
||||
model_config: ClassVar[ConfigDict] = ConfigDict(
|
||||
extra="forbid",
|
||||
frozen=True,
|
||||
str_strip_whitespace=True,
|
||||
)
|
||||
__all__ = ["DocsRegistry"]
|
||||
|
||||
|
||||
def frozen_mapping[K, V](value: Mapping[K, V] | None = None) -> Mapping[K, V]:
|
||||
@@ -39,3 +33,27 @@ def parse_docs_path(value: str | PurePosixPath) -> PurePosixPath:
|
||||
|
||||
|
||||
type DocsPath = Annotated[PurePosixPath, BeforeValidator(parse_docs_path)]
|
||||
|
||||
|
||||
def _empty_docs_mapping() -> Mapping[DocsPath, str]:
|
||||
return frozen_mapping()
|
||||
|
||||
|
||||
class DocsRegistry(BaseModel):
|
||||
"""In-memory index of documentation content."""
|
||||
|
||||
docs_markdown_by_path: Mapping[DocsPath, str] = Field(default_factory=_empty_docs_mapping)
|
||||
"""Maps each documentation path to its loaded Markdown content."""
|
||||
docs_markdown_path_index: tuple[DocsPath, ...]
|
||||
"""Lists documentation paths in deterministic index order."""
|
||||
|
||||
@field_validator("docs_markdown_by_path", mode="before")
|
||||
@classmethod
|
||||
def freeze_mappings(cls, value: Mapping[str, object] | None) -> Mapping[str, object]:
|
||||
return frozen_mapping(value)
|
||||
|
||||
model_config: ClassVar[ConfigDict] = ConfigDict(
|
||||
extra="forbid",
|
||||
frozen=True,
|
||||
str_strip_whitespace=True,
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
"""Pydantic models for the document registry."""
|
||||
@@ -1,26 +0,0 @@
|
||||
from collections.abc import Mapping
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic import field_validator
|
||||
|
||||
from .common import DocsPath
|
||||
from .common import StrictFrozenModel
|
||||
from .common import frozen_mapping
|
||||
|
||||
|
||||
def _empty_docs_mapping() -> Mapping[DocsPath, str]:
|
||||
return frozen_mapping()
|
||||
|
||||
|
||||
class DocsRegistry(StrictFrozenModel):
|
||||
"""In-memory index of documentation content."""
|
||||
|
||||
docs_markdown_by_path: Mapping[DocsPath, str] = Field(default_factory=_empty_docs_mapping)
|
||||
"""Maps each documentation path to its loaded Markdown content."""
|
||||
docs_markdown_path_index: tuple[DocsPath, ...]
|
||||
"""Lists documentation paths in deterministic index order."""
|
||||
|
||||
@field_validator("docs_markdown_by_path", mode="before")
|
||||
@classmethod
|
||||
def freeze_mappings(cls, value: Mapping[str, object] | None) -> Mapping[str, object]:
|
||||
return frozen_mapping(value)
|
||||
@@ -1,16 +0,0 @@
|
||||
from .models.common import parse_docs_path
|
||||
from .models.registry import DocsRegistry
|
||||
|
||||
|
||||
def read_docs_markdown_path(registry: DocsRegistry, path: str) -> dict[str, str]:
|
||||
docs_path = parse_docs_path(path)
|
||||
if docs_path.parts[0] == "skills":
|
||||
raise KeyError(f"unknown docs path: {docs_path.as_posix()}")
|
||||
if docs_path not in registry.docs_markdown_by_path:
|
||||
raise KeyError(f"unknown docs path: {docs_path.as_posix()}")
|
||||
return {
|
||||
"uri": f"resource://docs/{docs_path.as_posix()}",
|
||||
"format": "markdown",
|
||||
"source_path": f"docs/{docs_path.as_posix()}",
|
||||
"content": registry.docs_markdown_by_path[docs_path],
|
||||
}
|
||||
Reference in New Issue
Block a user