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.
Files changed (64) hide show
  1. ifcraftcorpus/cli.py +7 -2
  2. ifcraftcorpus/embeddings.py +11 -7
  3. ifcraftcorpus/index.py +4 -2
  4. ifcraftcorpus/mcp_server.py +316 -10
  5. ifcraftcorpus/providers.py +3 -3
  6. ifcraftcorpus/search.py +13 -9
  7. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/agent-design/agent_prompt_engineering.md +183 -9
  8. ifcraftcorpus-1.2.0.data/data/share/ifcraftcorpus/subagents/README.md +198 -0
  9. ifcraftcorpus-1.2.0.data/data/share/ifcraftcorpus/subagents/if_genre_consultant.md +257 -0
  10. ifcraftcorpus-1.2.0.data/data/share/ifcraftcorpus/subagents/if_platform_advisor.md +306 -0
  11. ifcraftcorpus-1.2.0.data/data/share/ifcraftcorpus/subagents/if_prose_writer.md +187 -0
  12. ifcraftcorpus-1.2.0.data/data/share/ifcraftcorpus/subagents/if_quality_reviewer.md +245 -0
  13. ifcraftcorpus-1.2.0.data/data/share/ifcraftcorpus/subagents/if_story_architect.md +162 -0
  14. ifcraftcorpus-1.2.0.data/data/share/ifcraftcorpus/subagents/if_world_curator.md +280 -0
  15. {ifcraftcorpus-1.1.0.dist-info → ifcraftcorpus-1.2.0.dist-info}/METADATA +1 -1
  16. ifcraftcorpus-1.2.0.dist-info/RECORD +66 -0
  17. ifcraftcorpus-1.1.0.dist-info/RECORD +0 -59
  18. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/agent-design/multi_agent_patterns.md +0 -0
  19. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/audience-and-access/accessibility_guidelines.md +0 -0
  20. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/audience-and-access/audience_targeting.md +0 -0
  21. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/audience-and-access/localization_considerations.md +0 -0
  22. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/audio_visual_integration.md +0 -0
  23. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/collaborative_if_writing.md +0 -0
  24. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/creative_workflow_pipeline.md +0 -0
  25. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/diegetic_design.md +0 -0
  26. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/idea_capture_and_hooks.md +0 -0
  27. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/if_platform_tools.md +0 -0
  28. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/player_analytics_metrics.md +0 -0
  29. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/quality_standards_if.md +0 -0
  30. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/research_and_verification.md +0 -0
  31. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/craft-foundations/testing_interactive_fiction.md +0 -0
  32. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/emotional-design/conflict_patterns.md +0 -0
  33. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/emotional-design/emotional_beats.md +0 -0
  34. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/game-design/mechanics_design_patterns.md +0 -0
  35. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/genre-conventions/children_and_ya_conventions.md +0 -0
  36. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/genre-conventions/fantasy_conventions.md +0 -0
  37. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/genre-conventions/historical_fiction.md +0 -0
  38. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/genre-conventions/horror_conventions.md +0 -0
  39. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/genre-conventions/mystery_conventions.md +0 -0
  40. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/genre-conventions/sci_fi_conventions.md +0 -0
  41. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/narrative-structure/branching_narrative_construction.md +0 -0
  42. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/narrative-structure/branching_narrative_craft.md +0 -0
  43. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/narrative-structure/endings_patterns.md +0 -0
  44. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/narrative-structure/episodic_serialized_if.md +0 -0
  45. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/narrative-structure/nonlinear_structure.md +0 -0
  46. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/narrative-structure/pacing_and_tension.md +0 -0
  47. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/narrative-structure/romance_and_relationships.md +0 -0
  48. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/narrative-structure/scene_structure_and_beats.md +0 -0
  49. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/narrative-structure/scene_transitions.md +0 -0
  50. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/prose-and-language/character_voice.md +0 -0
  51. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/prose-and-language/dialogue_craft.md +0 -0
  52. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/prose-and-language/exposition_techniques.md +0 -0
  53. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/prose-and-language/narrative_point_of_view.md +0 -0
  54. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/prose-and-language/prose_patterns.md +0 -0
  55. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/prose-and-language/subtext_and_implication.md +0 -0
  56. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/prose-and-language/voice_register_consistency.md +0 -0
  57. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/scope-and-planning/scope_and_length.md +0 -0
  58. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/world-and-setting/canon_management.md +0 -0
  59. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/world-and-setting/setting_as_character.md +0 -0
  60. {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.0.data}/data/share/ifcraftcorpus/corpus/world-and-setting/worldbuilding_patterns.md +0 -0
  61. {ifcraftcorpus-1.1.0.dist-info → ifcraftcorpus-1.2.0.dist-info}/WHEEL +0 -0
  62. {ifcraftcorpus-1.1.0.dist-info → ifcraftcorpus-1.2.0.dist-info}/entry_points.txt +0 -0
  63. {ifcraftcorpus-1.1.0.dist-info → ifcraftcorpus-1.2.0.dist-info}/licenses/LICENSE +0 -0
  64. {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
- return args.func(args)
280
+ result: int = args.func(args)
281
+ return result
277
282
 
278
283
 
279
284
  if __name__ == "__main__":
@@ -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
- return model.encode(texts, show_progress_bar=False, convert_to_numpy=True)
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
- return cursor.fetchone()[0]
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:
@@ -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",
@@ -21,7 +21,7 @@ from dataclasses import dataclass
21
21
  from typing import TYPE_CHECKING
22
22
 
23
23
  if TYPE_CHECKING:
24
- pass
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
- for r in sorted(results, key=lambda x: x.score, reverse=True):
451
- key = (r.document_name, r.section_heading)
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(r)
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