ifcraftcorpus 1.1.0__py3-none-any.whl → 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ifcraftcorpus/cli.py +7 -2
- ifcraftcorpus/embeddings.py +11 -7
- ifcraftcorpus/index.py +4 -2
- ifcraftcorpus/mcp_server.py +316 -10
- ifcraftcorpus/providers.py +3 -3
- ifcraftcorpus/search.py +13 -9
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/agent-design/agent_prompt_engineering.md +183 -9
- ifcraftcorpus-1.2.0.data/data/share/ifcraftcorpus/subagents/README.md +198 -0
- ifcraftcorpus-1.2.0.data/data/share/ifcraftcorpus/subagents/if_genre_consultant.md +257 -0
- ifcraftcorpus-1.2.0.data/data/share/ifcraftcorpus/subagents/if_platform_advisor.md +306 -0
- ifcraftcorpus-1.2.0.data/data/share/ifcraftcorpus/subagents/if_prose_writer.md +187 -0
- ifcraftcorpus-1.2.0.data/data/share/ifcraftcorpus/subagents/if_quality_reviewer.md +245 -0
- ifcraftcorpus-1.2.0.data/data/share/ifcraftcorpus/subagents/if_story_architect.md +162 -0
- ifcraftcorpus-1.2.0.data/data/share/ifcraftcorpus/subagents/if_world_curator.md +280 -0
- {ifcraftcorpus-1.1.0.dist-info → ifcraftcorpus-1.2.0.dist-info}/METADATA +1 -1
- ifcraftcorpus-1.2.0.dist-info/RECORD +66 -0
- ifcraftcorpus-1.1.0.dist-info/RECORD +0 -59
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/agent-design/multi_agent_patterns.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/audience-and-access/accessibility_guidelines.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/audience-and-access/audience_targeting.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/audience-and-access/localization_considerations.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/audio_visual_integration.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/collaborative_if_writing.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/creative_workflow_pipeline.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/diegetic_design.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/idea_capture_and_hooks.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/if_platform_tools.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/player_analytics_metrics.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/quality_standards_if.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/research_and_verification.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/testing_interactive_fiction.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/emotional-design/conflict_patterns.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/emotional-design/emotional_beats.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/game-design/mechanics_design_patterns.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/genre-conventions/children_and_ya_conventions.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/genre-conventions/fantasy_conventions.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/genre-conventions/historical_fiction.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/genre-conventions/horror_conventions.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/genre-conventions/mystery_conventions.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/genre-conventions/sci_fi_conventions.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/narrative-structure/branching_narrative_construction.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/narrative-structure/branching_narrative_craft.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/narrative-structure/endings_patterns.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/narrative-structure/episodic_serialized_if.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/narrative-structure/nonlinear_structure.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/narrative-structure/pacing_and_tension.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/narrative-structure/romance_and_relationships.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/narrative-structure/scene_structure_and_beats.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/narrative-structure/scene_transitions.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/prose-and-language/character_voice.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/prose-and-language/dialogue_craft.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/prose-and-language/exposition_techniques.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/prose-and-language/narrative_point_of_view.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/prose-and-language/prose_patterns.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/prose-and-language/subtext_and_implication.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/prose-and-language/voice_register_consistency.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/scope-and-planning/scope_and_length.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/world-and-setting/canon_management.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/world-and-setting/setting_as_character.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/world-and-setting/worldbuilding_patterns.md +0 -0
- {ifcraftcorpus-1.1.0.dist-info → ifcraftcorpus-1.2.0.dist-info}/WHEEL +0 -0
- {ifcraftcorpus-1.1.0.dist-info → ifcraftcorpus-1.2.0.dist-info}/entry_points.txt +0 -0
- {ifcraftcorpus-1.1.0.dist-info → ifcraftcorpus-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {ifcraftcorpus-1.1.0.dist-info → ifcraftcorpus-1.2.0.dist-info}/licenses/LICENSE-CONTENT +0 -0
ifcraftcorpus/cli.py
CHANGED
|
@@ -19,6 +19,10 @@ import argparse
|
|
|
19
19
|
import json
|
|
20
20
|
import sys
|
|
21
21
|
from pathlib import Path
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from ifcraftcorpus.providers import EmbeddingProvider
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
def cmd_info(args: argparse.Namespace) -> int:
|
|
@@ -129,7 +133,7 @@ def cmd_embeddings_build(args: argparse.Namespace) -> int:
|
|
|
129
133
|
)
|
|
130
134
|
|
|
131
135
|
# Get provider
|
|
132
|
-
provider = None
|
|
136
|
+
provider: EmbeddingProvider | None = None
|
|
133
137
|
if args.provider:
|
|
134
138
|
if args.provider == "ollama":
|
|
135
139
|
provider = OllamaEmbeddings(model=args.model, host=args.ollama_host)
|
|
@@ -273,7 +277,8 @@ def main() -> int:
|
|
|
273
277
|
emb_parser.print_help()
|
|
274
278
|
return 0
|
|
275
279
|
|
|
276
|
-
|
|
280
|
+
result: int = args.func(args)
|
|
281
|
+
return result
|
|
277
282
|
|
|
278
283
|
|
|
279
284
|
if __name__ == "__main__":
|
ifcraftcorpus/embeddings.py
CHANGED
|
@@ -44,10 +44,13 @@ from __future__ import annotations
|
|
|
44
44
|
import json
|
|
45
45
|
import logging
|
|
46
46
|
from pathlib import Path
|
|
47
|
-
from typing import TYPE_CHECKING
|
|
47
|
+
from typing import TYPE_CHECKING, Any
|
|
48
48
|
|
|
49
49
|
import numpy as np
|
|
50
50
|
|
|
51
|
+
if TYPE_CHECKING:
|
|
52
|
+
from sentence_transformers import SentenceTransformer
|
|
53
|
+
|
|
51
54
|
if TYPE_CHECKING:
|
|
52
55
|
from ifcraftcorpus.index import CorpusIndex
|
|
53
56
|
from ifcraftcorpus.providers import EmbeddingProvider
|
|
@@ -107,7 +110,8 @@ class EmbeddingIndex:
|
|
|
107
110
|
"""
|
|
108
111
|
self._provider = provider
|
|
109
112
|
self._embeddings: np.ndarray | None = None
|
|
110
|
-
self._metadata: list[dict] = []
|
|
113
|
+
self._metadata: list[dict[str, Any]] = []
|
|
114
|
+
self._st_model: SentenceTransformer | None = None
|
|
111
115
|
|
|
112
116
|
# For backward compatibility / persistence
|
|
113
117
|
if provider:
|
|
@@ -117,7 +121,6 @@ class EmbeddingIndex:
|
|
|
117
121
|
self.model_name = model_name
|
|
118
122
|
self._provider_name = "sentence-transformers"
|
|
119
123
|
# Lazy-load sentence-transformers model
|
|
120
|
-
self._st_model = None
|
|
121
124
|
if not lazy_load:
|
|
122
125
|
self._load_st_model()
|
|
123
126
|
|
|
@@ -126,7 +129,7 @@ class EmbeddingIndex:
|
|
|
126
129
|
"""Get the provider name."""
|
|
127
130
|
return self._provider_name
|
|
128
131
|
|
|
129
|
-
def _load_st_model(self):
|
|
132
|
+
def _load_st_model(self) -> SentenceTransformer:
|
|
130
133
|
"""Load sentence-transformers model (fallback)."""
|
|
131
134
|
if self._st_model is None:
|
|
132
135
|
try:
|
|
@@ -148,12 +151,13 @@ class EmbeddingIndex:
|
|
|
148
151
|
else:
|
|
149
152
|
# Fallback to sentence-transformers
|
|
150
153
|
model = self._load_st_model()
|
|
151
|
-
|
|
154
|
+
embeddings = model.encode(texts, show_progress_bar=False, convert_to_numpy=True)
|
|
155
|
+
return np.asarray(embeddings)
|
|
152
156
|
|
|
153
157
|
def add_texts(
|
|
154
158
|
self,
|
|
155
159
|
texts: list[str],
|
|
156
|
-
metadata: list[dict],
|
|
160
|
+
metadata: list[dict[str, Any]],
|
|
157
161
|
) -> None:
|
|
158
162
|
"""Add texts with metadata to the index.
|
|
159
163
|
|
|
@@ -185,7 +189,7 @@ class EmbeddingIndex:
|
|
|
185
189
|
*,
|
|
186
190
|
top_k: int = 10,
|
|
187
191
|
cluster: str | None = None,
|
|
188
|
-
) -> list[tuple[dict, float]]:
|
|
192
|
+
) -> list[tuple[dict[str, Any], float]]:
|
|
189
193
|
"""Search for semantically similar texts.
|
|
190
194
|
|
|
191
195
|
Args:
|
ifcraftcorpus/index.py
CHANGED
|
@@ -48,6 +48,7 @@ from __future__ import annotations
|
|
|
48
48
|
import sqlite3
|
|
49
49
|
from dataclasses import dataclass
|
|
50
50
|
from pathlib import Path
|
|
51
|
+
from typing import Any
|
|
51
52
|
|
|
52
53
|
from ifcraftcorpus.parser import Document, parse_directory
|
|
53
54
|
|
|
@@ -462,7 +463,7 @@ class CorpusIndex:
|
|
|
462
463
|
cursor = self.conn.execute("SELECT DISTINCT cluster FROM documents ORDER BY cluster")
|
|
463
464
|
return [row["cluster"] for row in cursor]
|
|
464
465
|
|
|
465
|
-
def get_document(self, name: str) -> dict | None:
|
|
466
|
+
def get_document(self, name: str) -> dict[str, Any] | None:
|
|
466
467
|
"""Get a document by name with all its sections.
|
|
467
468
|
|
|
468
469
|
Retrieves complete document data including metadata and all
|
|
@@ -535,7 +536,8 @@ class CorpusIndex:
|
|
|
535
536
|
Count of documents in the index.
|
|
536
537
|
"""
|
|
537
538
|
cursor = self.conn.execute("SELECT COUNT(*) FROM documents")
|
|
538
|
-
|
|
539
|
+
result = cursor.fetchone()
|
|
540
|
+
return int(result[0]) if result else 0
|
|
539
541
|
|
|
540
542
|
|
|
541
543
|
def build_index(corpus_dir: Path, output_path: Path) -> CorpusIndex:
|
ifcraftcorpus/mcp_server.py
CHANGED
|
@@ -10,6 +10,7 @@ Features:
|
|
|
10
10
|
- Retrieve specific documents
|
|
11
11
|
- List available documents and clusters
|
|
12
12
|
- Get corpus statistics
|
|
13
|
+
- Subagent prompts for IF authoring workflows
|
|
13
14
|
|
|
14
15
|
Installation:
|
|
15
16
|
The MCP server requires the ``mcp`` extra::
|
|
@@ -51,18 +52,72 @@ Tools:
|
|
|
51
52
|
corpus_stats: Get corpus statistics.
|
|
52
53
|
embeddings_status: Check embedding provider and index status.
|
|
53
54
|
build_embeddings: Build or rebuild semantic search embeddings.
|
|
55
|
+
|
|
56
|
+
Prompts:
|
|
57
|
+
if_story_architect: System prompt for an IF Story Architect agent.
|
|
58
|
+
if_prose_writer: System prompt for an IF Prose Writer agent.
|
|
59
|
+
if_quality_reviewer: System prompt for an IF Quality Reviewer agent.
|
|
60
|
+
if_genre_consultant: System prompt for an IF Genre Consultant agent.
|
|
61
|
+
if_world_curator: System prompt for an IF World Curator agent.
|
|
62
|
+
if_platform_advisor: System prompt for an IF Platform Advisor agent.
|
|
54
63
|
"""
|
|
55
64
|
|
|
56
65
|
from __future__ import annotations
|
|
57
66
|
|
|
58
67
|
import os
|
|
68
|
+
import sys
|
|
59
69
|
from pathlib import Path
|
|
60
|
-
from typing import Literal
|
|
70
|
+
from typing import Any, Literal
|
|
61
71
|
|
|
62
72
|
from fastmcp import FastMCP
|
|
73
|
+
from fastmcp.prompts import Message
|
|
74
|
+
from mcp.types import PromptMessage
|
|
63
75
|
|
|
64
76
|
from ifcraftcorpus.search import Corpus
|
|
65
77
|
|
|
78
|
+
|
|
79
|
+
def _get_subagents_dir() -> Path:
|
|
80
|
+
"""Get the path to the subagents directory.
|
|
81
|
+
|
|
82
|
+
Returns the subagents directory from either:
|
|
83
|
+
1. The installed package location (share/ifcraftcorpus/subagents)
|
|
84
|
+
2. The development location (project root/subagents)
|
|
85
|
+
"""
|
|
86
|
+
# Try installed location first
|
|
87
|
+
if sys.prefix != sys.base_prefix:
|
|
88
|
+
# We're in a virtual environment
|
|
89
|
+
installed_path = Path(sys.prefix) / "share" / "ifcraftcorpus" / "subagents"
|
|
90
|
+
if installed_path.exists():
|
|
91
|
+
return installed_path
|
|
92
|
+
|
|
93
|
+
# Try development location (relative to this file)
|
|
94
|
+
dev_path = Path(__file__).parent.parent.parent.parent / "subagents"
|
|
95
|
+
if dev_path.exists():
|
|
96
|
+
return dev_path
|
|
97
|
+
|
|
98
|
+
# Fallback to current directory
|
|
99
|
+
return Path("subagents")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _load_subagent_template(name: str) -> str:
|
|
103
|
+
"""Load a subagent template by name.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
name: Template name (without .md extension)
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The template content as a string.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
FileNotFoundError: If the template doesn't exist.
|
|
113
|
+
"""
|
|
114
|
+
subagents_dir = _get_subagents_dir()
|
|
115
|
+
template_path = subagents_dir / f"{name}.md"
|
|
116
|
+
if not template_path.exists():
|
|
117
|
+
raise FileNotFoundError(f"Subagent template not found: {template_path}")
|
|
118
|
+
return template_path.read_text(encoding="utf-8")
|
|
119
|
+
|
|
120
|
+
|
|
66
121
|
# Initialize FastMCP server
|
|
67
122
|
mcp = FastMCP(
|
|
68
123
|
name="IF Craft Corpus",
|
|
@@ -98,7 +153,7 @@ def search_corpus(
|
|
|
98
153
|
query: str,
|
|
99
154
|
cluster: str | None = None,
|
|
100
155
|
limit: int = 5,
|
|
101
|
-
) -> list[dict]:
|
|
156
|
+
) -> list[dict[str, Any]]:
|
|
102
157
|
"""Search the IF Craft Corpus for writing guidance.
|
|
103
158
|
|
|
104
159
|
Use this tool to find craft advice for interactive fiction writing,
|
|
@@ -136,7 +191,7 @@ def search_corpus(
|
|
|
136
191
|
|
|
137
192
|
|
|
138
193
|
@mcp.tool
|
|
139
|
-
def get_document(name: str) -> dict | None:
|
|
194
|
+
def get_document(name: str) -> dict[str, Any] | None:
|
|
140
195
|
"""Get a specific document from the IF Craft Corpus.
|
|
141
196
|
|
|
142
197
|
Use this tool when you need the full content of a known document,
|
|
@@ -154,7 +209,7 @@ def get_document(name: str) -> dict | None:
|
|
|
154
209
|
|
|
155
210
|
|
|
156
211
|
@mcp.tool
|
|
157
|
-
def list_documents(cluster: str | None = None) -> list[dict]:
|
|
212
|
+
def list_documents(cluster: str | None = None) -> list[dict[str, Any]]:
|
|
158
213
|
"""List all documents in the IF Craft Corpus.
|
|
159
214
|
|
|
160
215
|
Use this tool to discover what craft guidance is available.
|
|
@@ -175,7 +230,7 @@ def list_documents(cluster: str | None = None) -> list[dict]:
|
|
|
175
230
|
|
|
176
231
|
|
|
177
232
|
@mcp.tool
|
|
178
|
-
def list_clusters() -> list[dict]:
|
|
233
|
+
def list_clusters() -> list[dict[str, Any]]:
|
|
179
234
|
"""List all topic clusters in the IF Craft Corpus.
|
|
180
235
|
|
|
181
236
|
Each cluster groups related craft documents. Use this to understand
|
|
@@ -189,7 +244,7 @@ def list_clusters() -> list[dict]:
|
|
|
189
244
|
docs = corpus.list_documents()
|
|
190
245
|
|
|
191
246
|
# Count documents per cluster
|
|
192
|
-
counts = {}
|
|
247
|
+
counts: dict[str, int] = {}
|
|
193
248
|
for d in docs:
|
|
194
249
|
c = d["cluster"]
|
|
195
250
|
counts[c] = counts.get(c, 0) + 1
|
|
@@ -198,7 +253,7 @@ def list_clusters() -> list[dict]:
|
|
|
198
253
|
|
|
199
254
|
|
|
200
255
|
@mcp.tool
|
|
201
|
-
def corpus_stats() -> dict:
|
|
256
|
+
def corpus_stats() -> dict[str, Any]:
|
|
202
257
|
"""Get statistics about the IF Craft Corpus.
|
|
203
258
|
|
|
204
259
|
Returns:
|
|
@@ -214,7 +269,7 @@ def corpus_stats() -> dict:
|
|
|
214
269
|
|
|
215
270
|
|
|
216
271
|
@mcp.tool
|
|
217
|
-
def embeddings_status() -> dict:
|
|
272
|
+
def embeddings_status() -> dict[str, Any]:
|
|
218
273
|
"""Get status of embedding providers and index.
|
|
219
274
|
|
|
220
275
|
Returns information about available embedding providers (Ollama, OpenAI,
|
|
@@ -223,7 +278,7 @@ def embeddings_status() -> dict:
|
|
|
223
278
|
Returns:
|
|
224
279
|
Dict with provider availability and embedding index status.
|
|
225
280
|
"""
|
|
226
|
-
result = {
|
|
281
|
+
result: dict[str, Any] = {
|
|
227
282
|
"semantic_search_available": get_corpus().has_semantic_search,
|
|
228
283
|
"providers": {},
|
|
229
284
|
"saved_embeddings": None,
|
|
@@ -281,7 +336,7 @@ def embeddings_status() -> dict:
|
|
|
281
336
|
def build_embeddings(
|
|
282
337
|
provider: str | None = None,
|
|
283
338
|
force: bool = False,
|
|
284
|
-
) -> dict:
|
|
339
|
+
) -> dict[str, Any]:
|
|
285
340
|
"""Build or rebuild the embedding index for semantic search.
|
|
286
341
|
|
|
287
342
|
Builds embeddings for all corpus documents using the specified provider.
|
|
@@ -373,6 +428,257 @@ def build_embeddings(
|
|
|
373
428
|
}
|
|
374
429
|
|
|
375
430
|
|
|
431
|
+
# =============================================================================
|
|
432
|
+
# Subagent Prompts
|
|
433
|
+
# =============================================================================
|
|
434
|
+
#
|
|
435
|
+
# These prompts provide system prompts for specialized IF authoring agents.
|
|
436
|
+
# Each agent has a specific role in the IF creation workflow.
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@mcp.prompt(
|
|
440
|
+
name="if_story_architect",
|
|
441
|
+
description="System prompt for an IF Story Architect - an orchestrator agent that "
|
|
442
|
+
"plans narrative structure, decomposes IF projects, and coordinates creation.",
|
|
443
|
+
)
|
|
444
|
+
def if_story_architect_prompt(
|
|
445
|
+
project_name: str | None = None,
|
|
446
|
+
genre: str | None = None,
|
|
447
|
+
) -> list[PromptMessage]:
|
|
448
|
+
"""Get the IF Story Architect system prompt.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
project_name: Optional project name to include in the prompt context.
|
|
452
|
+
genre: Optional genre to emphasize (fantasy, horror, mystery, etc.).
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
System prompt messages for the Story Architect agent.
|
|
456
|
+
"""
|
|
457
|
+
template = _load_subagent_template("if_story_architect")
|
|
458
|
+
|
|
459
|
+
# Add optional context
|
|
460
|
+
context_parts = []
|
|
461
|
+
if project_name:
|
|
462
|
+
context_parts.append(f"Project: {project_name}")
|
|
463
|
+
if genre:
|
|
464
|
+
context_parts.append(f"Genre: {genre}")
|
|
465
|
+
|
|
466
|
+
if context_parts:
|
|
467
|
+
context = "\n\n---\n\n## Current Project Context\n\n" + "\n".join(context_parts)
|
|
468
|
+
template = template + context
|
|
469
|
+
|
|
470
|
+
return [Message(template, role="user")]
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@mcp.prompt(
|
|
474
|
+
name="if_prose_writer",
|
|
475
|
+
description="System prompt for an IF Prose Writer - a specialist agent that "
|
|
476
|
+
"creates narrative content including prose, dialogue, and scene text.",
|
|
477
|
+
)
|
|
478
|
+
def if_prose_writer_prompt(
|
|
479
|
+
genre: str | None = None,
|
|
480
|
+
pov: str | None = None,
|
|
481
|
+
) -> list[PromptMessage]:
|
|
482
|
+
"""Get the IF Prose Writer system prompt.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
genre: Optional genre to emphasize (fantasy, horror, mystery, etc.).
|
|
486
|
+
pov: Optional point of view (first, second, third).
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
System prompt messages for the Prose Writer agent.
|
|
490
|
+
"""
|
|
491
|
+
template = _load_subagent_template("if_prose_writer")
|
|
492
|
+
|
|
493
|
+
# Add optional context
|
|
494
|
+
context_parts = []
|
|
495
|
+
if genre:
|
|
496
|
+
context_parts.append(f"Genre: {genre}")
|
|
497
|
+
if pov:
|
|
498
|
+
context_parts.append(f"Point of View: {pov}")
|
|
499
|
+
|
|
500
|
+
if context_parts:
|
|
501
|
+
context = "\n\n---\n\n## Current Project Context\n\n" + "\n".join(context_parts)
|
|
502
|
+
template = template + context
|
|
503
|
+
|
|
504
|
+
return [Message(template, role="user")]
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
@mcp.prompt(
|
|
508
|
+
name="if_quality_reviewer",
|
|
509
|
+
description="System prompt for an IF Quality Reviewer - a validator agent that "
|
|
510
|
+
"reviews IF content for craft quality, consistency, and standards compliance.",
|
|
511
|
+
)
|
|
512
|
+
def if_quality_reviewer_prompt(
|
|
513
|
+
focus_areas: str | None = None,
|
|
514
|
+
) -> list[PromptMessage]:
|
|
515
|
+
"""Get the IF Quality Reviewer system prompt.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
focus_areas: Optional comma-separated list of areas to focus on
|
|
519
|
+
(e.g., "voice,pacing,continuity").
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
System prompt messages for the Quality Reviewer agent.
|
|
523
|
+
"""
|
|
524
|
+
template = _load_subagent_template("if_quality_reviewer")
|
|
525
|
+
|
|
526
|
+
if focus_areas:
|
|
527
|
+
context = f"\n\n---\n\n## Review Focus\n\nPrioritize these areas: {focus_areas}"
|
|
528
|
+
template = template + context
|
|
529
|
+
|
|
530
|
+
return [Message(template, role="user")]
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
@mcp.prompt(
|
|
534
|
+
name="if_genre_consultant",
|
|
535
|
+
description="System prompt for an IF Genre Consultant - a researcher agent that "
|
|
536
|
+
"provides genre-specific guidance on conventions, tropes, and reader expectations.",
|
|
537
|
+
)
|
|
538
|
+
def if_genre_consultant_prompt(
|
|
539
|
+
primary_genre: str | None = None,
|
|
540
|
+
secondary_genre: str | None = None,
|
|
541
|
+
) -> list[PromptMessage]:
|
|
542
|
+
"""Get the IF Genre Consultant system prompt.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
primary_genre: Optional primary genre to focus on.
|
|
546
|
+
secondary_genre: Optional secondary genre for blending advice.
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
System prompt messages for the Genre Consultant agent.
|
|
550
|
+
"""
|
|
551
|
+
template = _load_subagent_template("if_genre_consultant")
|
|
552
|
+
|
|
553
|
+
context_parts = []
|
|
554
|
+
if primary_genre:
|
|
555
|
+
context_parts.append(f"Primary Genre: {primary_genre}")
|
|
556
|
+
if secondary_genre:
|
|
557
|
+
context_parts.append(f"Secondary Genre: {secondary_genre}")
|
|
558
|
+
|
|
559
|
+
if context_parts:
|
|
560
|
+
context = "\n\n---\n\n## Genre Focus\n\n" + "\n".join(context_parts)
|
|
561
|
+
template = template + context
|
|
562
|
+
|
|
563
|
+
return [Message(template, role="user")]
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
@mcp.prompt(
|
|
567
|
+
name="if_world_curator",
|
|
568
|
+
description="System prompt for an IF World Curator - a curator agent that "
|
|
569
|
+
"maintains world consistency, manages canon, and ensures setting coherence.",
|
|
570
|
+
)
|
|
571
|
+
def if_world_curator_prompt(
|
|
572
|
+
world_name: str | None = None,
|
|
573
|
+
setting_type: str | None = None,
|
|
574
|
+
) -> list[PromptMessage]:
|
|
575
|
+
"""Get the IF World Curator system prompt.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
world_name: Optional name of the world/setting.
|
|
579
|
+
setting_type: Optional setting type (e.g., "fantasy medieval", "sci-fi space").
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
System prompt messages for the World Curator agent.
|
|
583
|
+
"""
|
|
584
|
+
template = _load_subagent_template("if_world_curator")
|
|
585
|
+
|
|
586
|
+
context_parts = []
|
|
587
|
+
if world_name:
|
|
588
|
+
context_parts.append(f"World: {world_name}")
|
|
589
|
+
if setting_type:
|
|
590
|
+
context_parts.append(f"Setting Type: {setting_type}")
|
|
591
|
+
|
|
592
|
+
if context_parts:
|
|
593
|
+
context = "\n\n---\n\n## World Context\n\n" + "\n".join(context_parts)
|
|
594
|
+
template = template + context
|
|
595
|
+
|
|
596
|
+
return [Message(template, role="user")]
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
@mcp.prompt(
|
|
600
|
+
name="if_platform_advisor",
|
|
601
|
+
description="System prompt for an IF Platform Advisor - a researcher agent that "
|
|
602
|
+
"provides guidance on tools, platforms, and technical implementation.",
|
|
603
|
+
)
|
|
604
|
+
def if_platform_advisor_prompt(
|
|
605
|
+
target_platform: str | None = None,
|
|
606
|
+
team_size: str | None = None,
|
|
607
|
+
) -> list[PromptMessage]:
|
|
608
|
+
"""Get the IF Platform Advisor system prompt.
|
|
609
|
+
|
|
610
|
+
Args:
|
|
611
|
+
target_platform: Optional target platform if already decided.
|
|
612
|
+
team_size: Optional team size (solo, small, large).
|
|
613
|
+
|
|
614
|
+
Returns:
|
|
615
|
+
System prompt messages for the Platform Advisor agent.
|
|
616
|
+
"""
|
|
617
|
+
template = _load_subagent_template("if_platform_advisor")
|
|
618
|
+
|
|
619
|
+
context_parts = []
|
|
620
|
+
if target_platform:
|
|
621
|
+
context_parts.append(f"Target Platform: {target_platform}")
|
|
622
|
+
if team_size:
|
|
623
|
+
context_parts.append(f"Team Size: {team_size}")
|
|
624
|
+
|
|
625
|
+
if context_parts:
|
|
626
|
+
context = "\n\n---\n\n## Project Context\n\n" + "\n".join(context_parts)
|
|
627
|
+
template = template + context
|
|
628
|
+
|
|
629
|
+
return [Message(template, role="user")]
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
@mcp.tool
|
|
633
|
+
def list_subagents() -> list[dict[str, Any]]:
|
|
634
|
+
"""List all available IF subagent prompts.
|
|
635
|
+
|
|
636
|
+
Returns a list of subagent templates that can be used as system prompts
|
|
637
|
+
for specialized IF authoring agents.
|
|
638
|
+
|
|
639
|
+
Returns:
|
|
640
|
+
List of subagents with name, description, and parameters.
|
|
641
|
+
"""
|
|
642
|
+
return [
|
|
643
|
+
{
|
|
644
|
+
"name": "if_story_architect",
|
|
645
|
+
"description": "Orchestrator that plans narrative structure and coordinates creation",
|
|
646
|
+
"archetype": "orchestrator",
|
|
647
|
+
"parameters": ["project_name", "genre"],
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
"name": "if_prose_writer",
|
|
651
|
+
"description": "Specialist that creates narrative prose, dialogue, and scene text",
|
|
652
|
+
"archetype": "creator",
|
|
653
|
+
"parameters": ["genre", "pov"],
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
"name": "if_quality_reviewer",
|
|
657
|
+
"description": "Validator agent that reviews content for quality and consistency",
|
|
658
|
+
"archetype": "validator",
|
|
659
|
+
"parameters": ["focus_areas"],
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
"name": "if_genre_consultant",
|
|
663
|
+
"description": "Researcher agent for genre conventions, tropes, and expectations",
|
|
664
|
+
"archetype": "researcher",
|
|
665
|
+
"parameters": ["primary_genre", "secondary_genre"],
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
"name": "if_world_curator",
|
|
669
|
+
"description": "Curator agent that maintains world consistency and canon",
|
|
670
|
+
"archetype": "curator",
|
|
671
|
+
"parameters": ["world_name", "setting_type"],
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
"name": "if_platform_advisor",
|
|
675
|
+
"description": "Researcher agent for tools, platforms, and technical implementation",
|
|
676
|
+
"archetype": "researcher",
|
|
677
|
+
"parameters": ["target_platform", "team_size"],
|
|
678
|
+
},
|
|
679
|
+
]
|
|
680
|
+
|
|
681
|
+
|
|
376
682
|
def run_server(
|
|
377
683
|
transport: Literal["stdio", "http"] = "stdio",
|
|
378
684
|
host: str = "127.0.0.1",
|
ifcraftcorpus/providers.py
CHANGED
|
@@ -21,7 +21,7 @@ from dataclasses import dataclass
|
|
|
21
21
|
from typing import TYPE_CHECKING
|
|
22
22
|
|
|
23
23
|
if TYPE_CHECKING:
|
|
24
|
-
|
|
24
|
+
from sentence_transformers import SentenceTransformer
|
|
25
25
|
|
|
26
26
|
logger = logging.getLogger(__name__)
|
|
27
27
|
|
|
@@ -314,12 +314,12 @@ class SentenceTransformersEmbeddings(EmbeddingProvider):
|
|
|
314
314
|
"""
|
|
315
315
|
self._model_name = model or DEFAULT_MODELS["sentence-transformers"]
|
|
316
316
|
self._dimension = MODEL_DIMENSIONS.get(self._model_name, 384)
|
|
317
|
-
self._model_instance = None
|
|
317
|
+
self._model_instance: SentenceTransformer | None = None
|
|
318
318
|
|
|
319
319
|
if not lazy_load:
|
|
320
320
|
self._load_model()
|
|
321
321
|
|
|
322
|
-
def _load_model(self):
|
|
322
|
+
def _load_model(self) -> SentenceTransformer:
|
|
323
323
|
"""Load the sentence transformer model."""
|
|
324
324
|
if self._model_instance is None:
|
|
325
325
|
try:
|
ifcraftcorpus/search.py
CHANGED
|
@@ -42,11 +42,12 @@ from __future__ import annotations
|
|
|
42
42
|
|
|
43
43
|
from dataclasses import dataclass
|
|
44
44
|
from pathlib import Path
|
|
45
|
-
from typing import TYPE_CHECKING, Literal
|
|
45
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
46
46
|
|
|
47
47
|
from ifcraftcorpus.index import CorpusIndex
|
|
48
48
|
|
|
49
49
|
if TYPE_CHECKING:
|
|
50
|
+
from ifcraftcorpus.embeddings import EmbeddingIndex
|
|
50
51
|
from ifcraftcorpus.providers import EmbeddingProvider
|
|
51
52
|
|
|
52
53
|
|
|
@@ -190,7 +191,7 @@ class Corpus:
|
|
|
190
191
|
self._use_bundled = use_bundled
|
|
191
192
|
|
|
192
193
|
self._fts_index: CorpusIndex | None = None
|
|
193
|
-
self._embedding_index = None # Lazy loaded
|
|
194
|
+
self._embedding_index: EmbeddingIndex | None = None # Lazy loaded
|
|
194
195
|
|
|
195
196
|
def _get_corpus_dir(self) -> Path:
|
|
196
197
|
"""Get the corpus directory path.
|
|
@@ -247,7 +248,7 @@ class Corpus:
|
|
|
247
248
|
self._fts_index.build_from_directory(corpus_dir)
|
|
248
249
|
return self._fts_index
|
|
249
250
|
|
|
250
|
-
def _get_embedding_index(self):
|
|
251
|
+
def _get_embedding_index(self) -> EmbeddingIndex | None:
|
|
251
252
|
"""Get the embedding index for semantic search.
|
|
252
253
|
|
|
253
254
|
Lazily loads the embedding index if embeddings_path was provided.
|
|
@@ -445,18 +446,21 @@ class Corpus:
|
|
|
445
446
|
|
|
446
447
|
# Deduplicate and sort by score
|
|
447
448
|
if mode == "hybrid":
|
|
448
|
-
seen = set()
|
|
449
|
-
unique_results = []
|
|
450
|
-
|
|
451
|
-
key
|
|
449
|
+
seen: set[tuple[str, str | None]] = set()
|
|
450
|
+
unique_results: list[CorpusResult] = []
|
|
451
|
+
sorted_results: list[CorpusResult] = sorted(
|
|
452
|
+
results, key=lambda x: x.score, reverse=True
|
|
453
|
+
)
|
|
454
|
+
for result in sorted_results:
|
|
455
|
+
key = (result.document_name, result.section_heading)
|
|
452
456
|
if key not in seen:
|
|
453
457
|
seen.add(key)
|
|
454
|
-
unique_results.append(
|
|
458
|
+
unique_results.append(result)
|
|
455
459
|
results = unique_results[:limit]
|
|
456
460
|
|
|
457
461
|
return results
|
|
458
462
|
|
|
459
|
-
def get_document(self, name: str) -> dict | None:
|
|
463
|
+
def get_document(self, name: str) -> dict[str, Any] | None:
|
|
460
464
|
"""Get a document by name with all its sections.
|
|
461
465
|
|
|
462
466
|
Retrieves complete document data including metadata and all
|