ifcraftcorpus 1.1.0__py3-none-any.whl → 1.2.1__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 +54 -5
- ifcraftcorpus/embeddings.py +11 -7
- ifcraftcorpus/index.py +26 -4
- ifcraftcorpus/logging_utils.py +84 -0
- ifcraftcorpus/mcp_server.py +418 -22
- ifcraftcorpus/providers.py +4 -4
- ifcraftcorpus/search.py +60 -12
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/agent-design/agent_prompt_engineering.md +183 -9
- ifcraftcorpus-1.2.1.data/data/share/ifcraftcorpus/subagents/README.md +198 -0
- ifcraftcorpus-1.2.1.data/data/share/ifcraftcorpus/subagents/if_genre_consultant.md +257 -0
- ifcraftcorpus-1.2.1.data/data/share/ifcraftcorpus/subagents/if_platform_advisor.md +306 -0
- ifcraftcorpus-1.2.1.data/data/share/ifcraftcorpus/subagents/if_prose_writer.md +187 -0
- ifcraftcorpus-1.2.1.data/data/share/ifcraftcorpus/subagents/if_quality_reviewer.md +245 -0
- ifcraftcorpus-1.2.1.data/data/share/ifcraftcorpus/subagents/if_story_architect.md +162 -0
- ifcraftcorpus-1.2.1.data/data/share/ifcraftcorpus/subagents/if_world_curator.md +280 -0
- {ifcraftcorpus-1.1.0.dist-info → ifcraftcorpus-1.2.1.dist-info}/METADATA +18 -1
- ifcraftcorpus-1.2.1.dist-info/RECORD +67 -0
- ifcraftcorpus-1.1.0.dist-info/RECORD +0 -59
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/agent-design/multi_agent_patterns.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/audience-and-access/accessibility_guidelines.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/audience-and-access/audience_targeting.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/audience-and-access/localization_considerations.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/craft-foundations/audio_visual_integration.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/craft-foundations/collaborative_if_writing.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/craft-foundations/creative_workflow_pipeline.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/craft-foundations/diegetic_design.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/craft-foundations/idea_capture_and_hooks.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/craft-foundations/if_platform_tools.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/craft-foundations/player_analytics_metrics.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/craft-foundations/quality_standards_if.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/craft-foundations/research_and_verification.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/craft-foundations/testing_interactive_fiction.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/emotional-design/conflict_patterns.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/emotional-design/emotional_beats.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/game-design/mechanics_design_patterns.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/genre-conventions/children_and_ya_conventions.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/genre-conventions/fantasy_conventions.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/genre-conventions/historical_fiction.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/genre-conventions/horror_conventions.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/genre-conventions/mystery_conventions.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/genre-conventions/sci_fi_conventions.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/narrative-structure/branching_narrative_construction.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/narrative-structure/branching_narrative_craft.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/narrative-structure/endings_patterns.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/narrative-structure/episodic_serialized_if.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/narrative-structure/nonlinear_structure.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/narrative-structure/pacing_and_tension.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/narrative-structure/romance_and_relationships.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/narrative-structure/scene_structure_and_beats.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/narrative-structure/scene_transitions.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/prose-and-language/character_voice.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/prose-and-language/dialogue_craft.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/prose-and-language/exposition_techniques.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/prose-and-language/narrative_point_of_view.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/prose-and-language/prose_patterns.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/prose-and-language/subtext_and_implication.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/prose-and-language/voice_register_consistency.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/scope-and-planning/scope_and_length.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/world-and-setting/canon_management.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/world-and-setting/setting_as_character.md +0 -0
- {ifcraftcorpus-1.1.0.data → ifcraftcorpus-1.2.1.data}/data/share/ifcraftcorpus/corpus/world-and-setting/worldbuilding_patterns.md +0 -0
- {ifcraftcorpus-1.1.0.dist-info → ifcraftcorpus-1.2.1.dist-info}/WHEEL +0 -0
- {ifcraftcorpus-1.1.0.dist-info → ifcraftcorpus-1.2.1.dist-info}/entry_points.txt +0 -0
- {ifcraftcorpus-1.1.0.dist-info → ifcraftcorpus-1.2.1.dist-info}/licenses/LICENSE +0 -0
- {ifcraftcorpus-1.1.0.dist-info → ifcraftcorpus-1.2.1.dist-info}/licenses/LICENSE-CONTENT +0 -0
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,94 @@ 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
|
|
|
67
|
+
import logging
|
|
58
68
|
import os
|
|
69
|
+
import sys
|
|
70
|
+
from collections.abc import Callable
|
|
59
71
|
from pathlib import Path
|
|
60
|
-
from typing import Literal
|
|
72
|
+
from typing import TYPE_CHECKING, Any, Literal, TypeVar
|
|
61
73
|
|
|
62
74
|
from fastmcp import FastMCP
|
|
75
|
+
from fastmcp.prompts import Message
|
|
76
|
+
from mcp.types import PromptMessage
|
|
63
77
|
|
|
78
|
+
from ifcraftcorpus.logging_utils import configure_logging
|
|
64
79
|
from ifcraftcorpus.search import Corpus
|
|
65
80
|
|
|
81
|
+
_CONFIGURED_LOG_LEVEL = configure_logging()
|
|
82
|
+
logger = logging.getLogger(__name__)
|
|
83
|
+
if _CONFIGURED_LOG_LEVEL is not None:
|
|
84
|
+
logger.info("MCP logging enabled at %s", logging.getLevelName(_CONFIGURED_LOG_LEVEL))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _truncate(value: str, limit: int = 200) -> str:
|
|
88
|
+
"""Truncate long strings for safe structured logging."""
|
|
89
|
+
|
|
90
|
+
if len(value) <= limit:
|
|
91
|
+
return value
|
|
92
|
+
return f"{value[:limit]}..."
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _get_subagents_dir() -> Path:
|
|
96
|
+
"""Get the path to the subagents directory.
|
|
97
|
+
|
|
98
|
+
Returns the subagents directory from either:
|
|
99
|
+
1. The installed package location (share/ifcraftcorpus/subagents)
|
|
100
|
+
2. The development location (project root/subagents)
|
|
101
|
+
"""
|
|
102
|
+
# Try installed location first
|
|
103
|
+
if sys.prefix != sys.base_prefix:
|
|
104
|
+
# We're in a virtual environment
|
|
105
|
+
installed_path = Path(sys.prefix) / "share" / "ifcraftcorpus" / "subagents"
|
|
106
|
+
if installed_path.exists():
|
|
107
|
+
logger.debug("Using installed subagents directory: %s", installed_path)
|
|
108
|
+
return installed_path
|
|
109
|
+
|
|
110
|
+
# Try development location (relative to this file)
|
|
111
|
+
dev_path = Path(__file__).parent.parent.parent.parent / "subagents"
|
|
112
|
+
if dev_path.exists():
|
|
113
|
+
logger.debug("Using development subagents directory: %s", dev_path)
|
|
114
|
+
return dev_path
|
|
115
|
+
|
|
116
|
+
# Fallback to current directory
|
|
117
|
+
fallback = Path("subagents")
|
|
118
|
+
logger.debug("Using fallback subagents directory: %s", fallback)
|
|
119
|
+
return fallback
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _load_subagent_template(name: str) -> str:
|
|
123
|
+
"""Load a subagent template by name.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
name: Template name (without .md extension)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
The template content as a string.
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
FileNotFoundError: If the template doesn't exist.
|
|
133
|
+
"""
|
|
134
|
+
subagents_dir = _get_subagents_dir()
|
|
135
|
+
template_path = subagents_dir / f"{name}.md"
|
|
136
|
+
if not template_path.exists():
|
|
137
|
+
logger.error("Subagent template missing: %s", template_path)
|
|
138
|
+
raise FileNotFoundError(f"Subagent template not found: {template_path}")
|
|
139
|
+
logger.debug("Loaded subagent template %s", template_path.name)
|
|
140
|
+
return template_path.read_text(encoding="utf-8")
|
|
141
|
+
|
|
142
|
+
|
|
66
143
|
# Initialize FastMCP server
|
|
67
144
|
mcp = FastMCP(
|
|
68
145
|
name="IF Craft Corpus",
|
|
@@ -74,6 +151,16 @@ mcp = FastMCP(
|
|
|
74
151
|
""",
|
|
75
152
|
)
|
|
76
153
|
|
|
154
|
+
if TYPE_CHECKING:
|
|
155
|
+
TCallable = TypeVar("TCallable", bound=Callable[..., Any])
|
|
156
|
+
|
|
157
|
+
def tool(func: TCallable, /) -> TCallable: ...
|
|
158
|
+
|
|
159
|
+
def prompt(*args: Any, **kwargs: Any) -> Callable[[TCallable], TCallable]: ...
|
|
160
|
+
else: # pragma: no cover - runtime aliases for decorators
|
|
161
|
+
tool = mcp.tool
|
|
162
|
+
prompt = mcp.prompt
|
|
163
|
+
|
|
77
164
|
# Global corpus instance (initialized on first use)
|
|
78
165
|
_corpus: Corpus | None = None
|
|
79
166
|
|
|
@@ -89,16 +176,17 @@ def get_corpus() -> Corpus:
|
|
|
89
176
|
"""
|
|
90
177
|
global _corpus
|
|
91
178
|
if _corpus is None:
|
|
179
|
+
logger.info("Initializing shared Corpus instance for MCP server")
|
|
92
180
|
_corpus = Corpus()
|
|
93
181
|
return _corpus
|
|
94
182
|
|
|
95
183
|
|
|
96
|
-
@
|
|
184
|
+
@tool
|
|
97
185
|
def search_corpus(
|
|
98
186
|
query: str,
|
|
99
187
|
cluster: str | None = None,
|
|
100
188
|
limit: int = 5,
|
|
101
|
-
) -> list[dict]:
|
|
189
|
+
) -> list[dict[str, Any]]:
|
|
102
190
|
"""Search the IF Craft Corpus for writing guidance.
|
|
103
191
|
|
|
104
192
|
Use this tool to find craft advice for interactive fiction writing,
|
|
@@ -120,8 +208,21 @@ def search_corpus(
|
|
|
120
208
|
"""
|
|
121
209
|
limit = max(1, min(20, limit))
|
|
122
210
|
|
|
211
|
+
logger.debug(
|
|
212
|
+
"search_corpus(query=%r, cluster=%s, limit=%s)",
|
|
213
|
+
_truncate(query),
|
|
214
|
+
cluster,
|
|
215
|
+
limit,
|
|
216
|
+
)
|
|
217
|
+
|
|
123
218
|
corpus = get_corpus()
|
|
124
|
-
|
|
219
|
+
try:
|
|
220
|
+
results = corpus.search(query, cluster=cluster, limit=limit)
|
|
221
|
+
except Exception: # pragma: no cover - defensive logging
|
|
222
|
+
logger.exception("search_corpus failed")
|
|
223
|
+
raise
|
|
224
|
+
|
|
225
|
+
logger.debug("search_corpus returning %s results", len(results))
|
|
125
226
|
|
|
126
227
|
return [
|
|
127
228
|
{
|
|
@@ -135,8 +236,8 @@ def search_corpus(
|
|
|
135
236
|
]
|
|
136
237
|
|
|
137
238
|
|
|
138
|
-
@
|
|
139
|
-
def get_document(name: str) -> dict | None:
|
|
239
|
+
@tool
|
|
240
|
+
def get_document(name: str) -> dict[str, Any] | None:
|
|
140
241
|
"""Get a specific document from the IF Craft Corpus.
|
|
141
242
|
|
|
142
243
|
Use this tool when you need the full content of a known document,
|
|
@@ -149,12 +250,18 @@ def get_document(name: str) -> dict | None:
|
|
|
149
250
|
Returns:
|
|
150
251
|
Full document with title, summary, cluster, topics, and all sections.
|
|
151
252
|
"""
|
|
253
|
+
logger.debug("get_document(%s)", name)
|
|
152
254
|
corpus = get_corpus()
|
|
153
|
-
|
|
255
|
+
document = corpus.get_document(name)
|
|
256
|
+
if document is None:
|
|
257
|
+
logger.info("Document not found: %s", name)
|
|
258
|
+
else:
|
|
259
|
+
logger.debug("Document %s retrieved", name)
|
|
260
|
+
return document
|
|
154
261
|
|
|
155
262
|
|
|
156
|
-
@
|
|
157
|
-
def list_documents(cluster: str | None = None) -> list[dict]:
|
|
263
|
+
@tool
|
|
264
|
+
def list_documents(cluster: str | None = None) -> list[dict[str, Any]]:
|
|
158
265
|
"""List all documents in the IF Craft Corpus.
|
|
159
266
|
|
|
160
267
|
Use this tool to discover what craft guidance is available.
|
|
@@ -165,17 +272,19 @@ def list_documents(cluster: str | None = None) -> list[dict]:
|
|
|
165
272
|
Returns:
|
|
166
273
|
List of documents with name, title, cluster, and topics.
|
|
167
274
|
"""
|
|
275
|
+
logger.debug("list_documents(cluster=%s)", cluster)
|
|
168
276
|
corpus = get_corpus()
|
|
169
277
|
docs = corpus.list_documents()
|
|
170
278
|
|
|
171
279
|
if cluster:
|
|
172
280
|
docs = [d for d in docs if d["cluster"] == cluster]
|
|
173
281
|
|
|
282
|
+
logger.debug("list_documents returning %s entries", len(docs))
|
|
174
283
|
return docs
|
|
175
284
|
|
|
176
285
|
|
|
177
|
-
@
|
|
178
|
-
def list_clusters() -> list[dict]:
|
|
286
|
+
@tool
|
|
287
|
+
def list_clusters() -> list[dict[str, Any]]:
|
|
179
288
|
"""List all topic clusters in the IF Craft Corpus.
|
|
180
289
|
|
|
181
290
|
Each cluster groups related craft documents. Use this to understand
|
|
@@ -184,37 +293,48 @@ def list_clusters() -> list[dict]:
|
|
|
184
293
|
Returns:
|
|
185
294
|
List of clusters with names and document counts.
|
|
186
295
|
"""
|
|
296
|
+
logger.debug("list_clusters invoked")
|
|
187
297
|
corpus = get_corpus()
|
|
188
298
|
clusters = corpus.list_clusters()
|
|
189
299
|
docs = corpus.list_documents()
|
|
190
300
|
|
|
191
301
|
# Count documents per cluster
|
|
192
|
-
counts = {}
|
|
302
|
+
counts: dict[str, int] = {}
|
|
193
303
|
for d in docs:
|
|
194
304
|
c = d["cluster"]
|
|
195
305
|
counts[c] = counts.get(c, 0) + 1
|
|
196
306
|
|
|
197
|
-
|
|
307
|
+
cluster_info = [{"name": c, "document_count": counts.get(c, 0)} for c in clusters]
|
|
308
|
+
logger.debug("list_clusters returning %s clusters", len(cluster_info))
|
|
309
|
+
return cluster_info
|
|
198
310
|
|
|
199
311
|
|
|
200
|
-
@
|
|
201
|
-
def corpus_stats() -> dict:
|
|
312
|
+
@tool
|
|
313
|
+
def corpus_stats() -> dict[str, Any]:
|
|
202
314
|
"""Get statistics about the IF Craft Corpus.
|
|
203
315
|
|
|
204
316
|
Returns:
|
|
205
317
|
Statistics including document count, cluster count, and availability.
|
|
206
318
|
"""
|
|
319
|
+
logger.debug("corpus_stats invoked")
|
|
207
320
|
corpus = get_corpus()
|
|
208
|
-
|
|
321
|
+
stats = {
|
|
209
322
|
"document_count": corpus.document_count(),
|
|
210
323
|
"cluster_count": len(corpus.list_clusters()),
|
|
211
324
|
"clusters": corpus.list_clusters(),
|
|
212
325
|
"semantic_search_available": corpus.has_semantic_search,
|
|
213
326
|
}
|
|
327
|
+
logger.debug(
|
|
328
|
+
"corpus_stats: docs=%s clusters=%s semantic=%s",
|
|
329
|
+
stats["document_count"],
|
|
330
|
+
stats["cluster_count"],
|
|
331
|
+
stats["semantic_search_available"],
|
|
332
|
+
)
|
|
333
|
+
return stats
|
|
214
334
|
|
|
215
335
|
|
|
216
|
-
@
|
|
217
|
-
def embeddings_status() -> dict:
|
|
336
|
+
@tool
|
|
337
|
+
def embeddings_status() -> dict[str, Any]:
|
|
218
338
|
"""Get status of embedding providers and index.
|
|
219
339
|
|
|
220
340
|
Returns information about available embedding providers (Ollama, OpenAI,
|
|
@@ -223,7 +343,8 @@ def embeddings_status() -> dict:
|
|
|
223
343
|
Returns:
|
|
224
344
|
Dict with provider availability and embedding index status.
|
|
225
345
|
"""
|
|
226
|
-
|
|
346
|
+
logger.debug("embeddings_status invoked")
|
|
347
|
+
result: dict[str, Any] = {
|
|
227
348
|
"semantic_search_available": get_corpus().has_semantic_search,
|
|
228
349
|
"providers": {},
|
|
229
350
|
"saved_embeddings": None,
|
|
@@ -251,13 +372,15 @@ def embeddings_status() -> dict:
|
|
|
251
372
|
"model": provider.model if available else None,
|
|
252
373
|
"dimension": provider.dimension if available else None,
|
|
253
374
|
}
|
|
254
|
-
except Exception:
|
|
375
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
376
|
+
logger.warning("Failed to inspect embedding provider %s: %s", name, exc)
|
|
255
377
|
result["providers"][name] = {"available": False, "error": "import_failed"}
|
|
256
378
|
|
|
257
379
|
# Auto-detect best provider
|
|
258
380
|
auto = get_embedding_provider()
|
|
259
381
|
result["auto_detected_provider"] = auto.provider_name if auto else None
|
|
260
382
|
except ImportError:
|
|
383
|
+
logger.warning("Embedding providers module not importable for status call")
|
|
261
384
|
result["providers_error"] = "providers module not available"
|
|
262
385
|
|
|
263
386
|
# Check for saved embeddings
|
|
@@ -274,14 +397,20 @@ def embeddings_status() -> dict:
|
|
|
274
397
|
"count": len(meta.get("metadata", [])),
|
|
275
398
|
}
|
|
276
399
|
|
|
400
|
+
logger.debug(
|
|
401
|
+
"embeddings_status semantic=%s providers=%s saved=%s",
|
|
402
|
+
result["semantic_search_available"],
|
|
403
|
+
list(result["providers"].keys()),
|
|
404
|
+
bool(result["saved_embeddings"]),
|
|
405
|
+
)
|
|
277
406
|
return result
|
|
278
407
|
|
|
279
408
|
|
|
280
|
-
@
|
|
409
|
+
@tool
|
|
281
410
|
def build_embeddings(
|
|
282
411
|
provider: str | None = None,
|
|
283
412
|
force: bool = False,
|
|
284
|
-
) -> dict:
|
|
413
|
+
) -> dict[str, Any]:
|
|
285
414
|
"""Build or rebuild the embedding index for semantic search.
|
|
286
415
|
|
|
287
416
|
Builds embeddings for all corpus documents using the specified provider.
|
|
@@ -303,6 +432,8 @@ def build_embeddings(
|
|
|
303
432
|
"""
|
|
304
433
|
global _corpus
|
|
305
434
|
|
|
435
|
+
logger.info("build_embeddings requested provider=%s force=%s", provider, force)
|
|
436
|
+
|
|
306
437
|
try:
|
|
307
438
|
from ifcraftcorpus.providers import (
|
|
308
439
|
OllamaEmbeddings,
|
|
@@ -311,6 +442,7 @@ def build_embeddings(
|
|
|
311
442
|
get_embedding_provider,
|
|
312
443
|
)
|
|
313
444
|
except ImportError:
|
|
445
|
+
logger.warning("Embedding provider modules not installed")
|
|
314
446
|
return {
|
|
315
447
|
"error": "Embedding providers not available. "
|
|
316
448
|
"Install with [embeddings-api] or [embeddings] extras."
|
|
@@ -325,6 +457,7 @@ def build_embeddings(
|
|
|
325
457
|
"sentence_transformers": SentenceTransformersEmbeddings,
|
|
326
458
|
}
|
|
327
459
|
if provider not in provider_map:
|
|
460
|
+
logger.warning("Unknown embeddings provider requested: %s", provider)
|
|
328
461
|
return {
|
|
329
462
|
"error": f"Unknown provider: {provider}. Use: ollama, openai, sentence_transformers"
|
|
330
463
|
}
|
|
@@ -333,12 +466,14 @@ def build_embeddings(
|
|
|
333
466
|
embedding_provider = get_embedding_provider()
|
|
334
467
|
|
|
335
468
|
if not embedding_provider:
|
|
469
|
+
logger.warning("No embedding provider available for build request")
|
|
336
470
|
return {
|
|
337
471
|
"error": "No embedding provider available. "
|
|
338
472
|
"Configure Ollama, set OPENAI_API_KEY, or install sentence-transformers."
|
|
339
473
|
}
|
|
340
474
|
|
|
341
475
|
if not embedding_provider.check_availability():
|
|
476
|
+
logger.warning("Embedding provider %s unavailable", embedding_provider.provider_name)
|
|
342
477
|
return {"error": f"Provider {embedding_provider.provider_name} is not available."}
|
|
343
478
|
|
|
344
479
|
# Configure paths
|
|
@@ -346,6 +481,7 @@ def build_embeddings(
|
|
|
346
481
|
|
|
347
482
|
# Check if already exists
|
|
348
483
|
if not force and embeddings_path.exists() and (embeddings_path / "metadata.json").exists():
|
|
484
|
+
logger.info("Embedding build skipped; existing index at %s", embeddings_path)
|
|
349
485
|
return {
|
|
350
486
|
"status": "skipped",
|
|
351
487
|
"message": "Embeddings already exist. Use force=True to rebuild.",
|
|
@@ -360,6 +496,12 @@ def build_embeddings(
|
|
|
360
496
|
|
|
361
497
|
# Build embeddings
|
|
362
498
|
count = corpus.build_embeddings(force=force)
|
|
499
|
+
logger.info(
|
|
500
|
+
"Embedding build complete items=%s provider=%s model=%s",
|
|
501
|
+
count,
|
|
502
|
+
embedding_provider.provider_name,
|
|
503
|
+
embedding_provider.model,
|
|
504
|
+
)
|
|
363
505
|
|
|
364
506
|
# Update global corpus to use new embeddings
|
|
365
507
|
_corpus = Corpus(embeddings_path=embeddings_path)
|
|
@@ -373,6 +515,258 @@ def build_embeddings(
|
|
|
373
515
|
}
|
|
374
516
|
|
|
375
517
|
|
|
518
|
+
# =============================================================================
|
|
519
|
+
# Subagent Prompts
|
|
520
|
+
# =============================================================================
|
|
521
|
+
#
|
|
522
|
+
# These prompts provide system prompts for specialized IF authoring agents.
|
|
523
|
+
# Each agent has a specific role in the IF creation workflow.
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
@prompt(
|
|
527
|
+
name="if_story_architect",
|
|
528
|
+
description="System prompt for an IF Story Architect - an orchestrator agent that "
|
|
529
|
+
"plans narrative structure, decomposes IF projects, and coordinates creation.",
|
|
530
|
+
)
|
|
531
|
+
def if_story_architect_prompt(
|
|
532
|
+
project_name: str | None = None,
|
|
533
|
+
genre: str | None = None,
|
|
534
|
+
) -> list[PromptMessage]:
|
|
535
|
+
"""Get the IF Story Architect system prompt.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
project_name: Optional project name to include in the prompt context.
|
|
539
|
+
genre: Optional genre to emphasize (fantasy, horror, mystery, etc.).
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
System prompt messages for the Story Architect agent.
|
|
543
|
+
"""
|
|
544
|
+
template = _load_subagent_template("if_story_architect")
|
|
545
|
+
|
|
546
|
+
# Add optional context
|
|
547
|
+
context_parts = []
|
|
548
|
+
if project_name:
|
|
549
|
+
context_parts.append(f"Project: {project_name}")
|
|
550
|
+
if genre:
|
|
551
|
+
context_parts.append(f"Genre: {genre}")
|
|
552
|
+
|
|
553
|
+
if context_parts:
|
|
554
|
+
context = "\n\n---\n\n## Current Project Context\n\n" + "\n".join(context_parts)
|
|
555
|
+
template = template + context
|
|
556
|
+
|
|
557
|
+
return [Message(template, role="user")]
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
@prompt(
|
|
561
|
+
name="if_prose_writer",
|
|
562
|
+
description="System prompt for an IF Prose Writer - a specialist agent that "
|
|
563
|
+
"creates narrative content including prose, dialogue, and scene text.",
|
|
564
|
+
)
|
|
565
|
+
def if_prose_writer_prompt(
|
|
566
|
+
genre: str | None = None,
|
|
567
|
+
pov: str | None = None,
|
|
568
|
+
) -> list[PromptMessage]:
|
|
569
|
+
"""Get the IF Prose Writer system prompt.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
genre: Optional genre to emphasize (fantasy, horror, mystery, etc.).
|
|
573
|
+
pov: Optional point of view (first, second, third).
|
|
574
|
+
|
|
575
|
+
Returns:
|
|
576
|
+
System prompt messages for the Prose Writer agent.
|
|
577
|
+
"""
|
|
578
|
+
template = _load_subagent_template("if_prose_writer")
|
|
579
|
+
|
|
580
|
+
# Add optional context
|
|
581
|
+
context_parts = []
|
|
582
|
+
if genre:
|
|
583
|
+
context_parts.append(f"Genre: {genre}")
|
|
584
|
+
if pov:
|
|
585
|
+
context_parts.append(f"Point of View: {pov}")
|
|
586
|
+
|
|
587
|
+
if context_parts:
|
|
588
|
+
context = "\n\n---\n\n## Current Project Context\n\n" + "\n".join(context_parts)
|
|
589
|
+
template = template + context
|
|
590
|
+
|
|
591
|
+
return [Message(template, role="user")]
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
@prompt(
|
|
595
|
+
name="if_quality_reviewer",
|
|
596
|
+
description="System prompt for an IF Quality Reviewer - a validator agent that "
|
|
597
|
+
"reviews IF content for craft quality, consistency, and standards compliance.",
|
|
598
|
+
)
|
|
599
|
+
def if_quality_reviewer_prompt(
|
|
600
|
+
focus_areas: str | None = None,
|
|
601
|
+
) -> list[PromptMessage]:
|
|
602
|
+
"""Get the IF Quality Reviewer system prompt.
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
focus_areas: Optional comma-separated list of areas to focus on
|
|
606
|
+
(e.g., "voice,pacing,continuity").
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
System prompt messages for the Quality Reviewer agent.
|
|
610
|
+
"""
|
|
611
|
+
template = _load_subagent_template("if_quality_reviewer")
|
|
612
|
+
|
|
613
|
+
if focus_areas:
|
|
614
|
+
context = f"\n\n---\n\n## Review Focus\n\nPrioritize these areas: {focus_areas}"
|
|
615
|
+
template = template + context
|
|
616
|
+
|
|
617
|
+
return [Message(template, role="user")]
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
@prompt(
|
|
621
|
+
name="if_genre_consultant",
|
|
622
|
+
description="System prompt for an IF Genre Consultant - a researcher agent that "
|
|
623
|
+
"provides genre-specific guidance on conventions, tropes, and reader expectations.",
|
|
624
|
+
)
|
|
625
|
+
def if_genre_consultant_prompt(
|
|
626
|
+
primary_genre: str | None = None,
|
|
627
|
+
secondary_genre: str | None = None,
|
|
628
|
+
) -> list[PromptMessage]:
|
|
629
|
+
"""Get the IF Genre Consultant system prompt.
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
primary_genre: Optional primary genre to focus on.
|
|
633
|
+
secondary_genre: Optional secondary genre for blending advice.
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
System prompt messages for the Genre Consultant agent.
|
|
637
|
+
"""
|
|
638
|
+
template = _load_subagent_template("if_genre_consultant")
|
|
639
|
+
|
|
640
|
+
context_parts = []
|
|
641
|
+
if primary_genre:
|
|
642
|
+
context_parts.append(f"Primary Genre: {primary_genre}")
|
|
643
|
+
if secondary_genre:
|
|
644
|
+
context_parts.append(f"Secondary Genre: {secondary_genre}")
|
|
645
|
+
|
|
646
|
+
if context_parts:
|
|
647
|
+
context = "\n\n---\n\n## Genre Focus\n\n" + "\n".join(context_parts)
|
|
648
|
+
template = template + context
|
|
649
|
+
|
|
650
|
+
return [Message(template, role="user")]
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
@prompt(
|
|
654
|
+
name="if_world_curator",
|
|
655
|
+
description="System prompt for an IF World Curator - a curator agent that "
|
|
656
|
+
"maintains world consistency, manages canon, and ensures setting coherence.",
|
|
657
|
+
)
|
|
658
|
+
def if_world_curator_prompt(
|
|
659
|
+
world_name: str | None = None,
|
|
660
|
+
setting_type: str | None = None,
|
|
661
|
+
) -> list[PromptMessage]:
|
|
662
|
+
"""Get the IF World Curator system prompt.
|
|
663
|
+
|
|
664
|
+
Args:
|
|
665
|
+
world_name: Optional name of the world/setting.
|
|
666
|
+
setting_type: Optional setting type (e.g., "fantasy medieval", "sci-fi space").
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
System prompt messages for the World Curator agent.
|
|
670
|
+
"""
|
|
671
|
+
template = _load_subagent_template("if_world_curator")
|
|
672
|
+
|
|
673
|
+
context_parts = []
|
|
674
|
+
if world_name:
|
|
675
|
+
context_parts.append(f"World: {world_name}")
|
|
676
|
+
if setting_type:
|
|
677
|
+
context_parts.append(f"Setting Type: {setting_type}")
|
|
678
|
+
|
|
679
|
+
if context_parts:
|
|
680
|
+
context = "\n\n---\n\n## World Context\n\n" + "\n".join(context_parts)
|
|
681
|
+
template = template + context
|
|
682
|
+
|
|
683
|
+
return [Message(template, role="user")]
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
@prompt(
|
|
687
|
+
name="if_platform_advisor",
|
|
688
|
+
description="System prompt for an IF Platform Advisor - a researcher agent that "
|
|
689
|
+
"provides guidance on tools, platforms, and technical implementation.",
|
|
690
|
+
)
|
|
691
|
+
def if_platform_advisor_prompt(
|
|
692
|
+
target_platform: str | None = None,
|
|
693
|
+
team_size: str | None = None,
|
|
694
|
+
) -> list[PromptMessage]:
|
|
695
|
+
"""Get the IF Platform Advisor system prompt.
|
|
696
|
+
|
|
697
|
+
Args:
|
|
698
|
+
target_platform: Optional target platform if already decided.
|
|
699
|
+
team_size: Optional team size (solo, small, large).
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
System prompt messages for the Platform Advisor agent.
|
|
703
|
+
"""
|
|
704
|
+
template = _load_subagent_template("if_platform_advisor")
|
|
705
|
+
|
|
706
|
+
context_parts = []
|
|
707
|
+
if target_platform:
|
|
708
|
+
context_parts.append(f"Target Platform: {target_platform}")
|
|
709
|
+
if team_size:
|
|
710
|
+
context_parts.append(f"Team Size: {team_size}")
|
|
711
|
+
|
|
712
|
+
if context_parts:
|
|
713
|
+
context = "\n\n---\n\n## Project Context\n\n" + "\n".join(context_parts)
|
|
714
|
+
template = template + context
|
|
715
|
+
|
|
716
|
+
return [Message(template, role="user")]
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
@tool
|
|
720
|
+
def list_subagents() -> list[dict[str, Any]]:
|
|
721
|
+
"""List all available IF subagent prompts.
|
|
722
|
+
|
|
723
|
+
Returns a list of subagent templates that can be used as system prompts
|
|
724
|
+
for specialized IF authoring agents.
|
|
725
|
+
|
|
726
|
+
Returns:
|
|
727
|
+
List of subagents with name, description, and parameters.
|
|
728
|
+
"""
|
|
729
|
+
logger.debug("list_subagents invoked")
|
|
730
|
+
return [
|
|
731
|
+
{
|
|
732
|
+
"name": "if_story_architect",
|
|
733
|
+
"description": "Orchestrator that plans narrative structure and coordinates creation",
|
|
734
|
+
"archetype": "orchestrator",
|
|
735
|
+
"parameters": ["project_name", "genre"],
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
"name": "if_prose_writer",
|
|
739
|
+
"description": "Specialist that creates narrative prose, dialogue, and scene text",
|
|
740
|
+
"archetype": "creator",
|
|
741
|
+
"parameters": ["genre", "pov"],
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
"name": "if_quality_reviewer",
|
|
745
|
+
"description": "Validator agent that reviews content for quality and consistency",
|
|
746
|
+
"archetype": "validator",
|
|
747
|
+
"parameters": ["focus_areas"],
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
"name": "if_genre_consultant",
|
|
751
|
+
"description": "Researcher agent for genre conventions, tropes, and expectations",
|
|
752
|
+
"archetype": "researcher",
|
|
753
|
+
"parameters": ["primary_genre", "secondary_genre"],
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
"name": "if_world_curator",
|
|
757
|
+
"description": "Curator agent that maintains world consistency and canon",
|
|
758
|
+
"archetype": "curator",
|
|
759
|
+
"parameters": ["world_name", "setting_type"],
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
"name": "if_platform_advisor",
|
|
763
|
+
"description": "Researcher agent for tools, platforms, and technical implementation",
|
|
764
|
+
"archetype": "researcher",
|
|
765
|
+
"parameters": ["target_platform", "team_size"],
|
|
766
|
+
},
|
|
767
|
+
]
|
|
768
|
+
|
|
769
|
+
|
|
376
770
|
def run_server(
|
|
377
771
|
transport: Literal["stdio", "http"] = "stdio",
|
|
378
772
|
host: str = "127.0.0.1",
|
|
@@ -400,8 +794,10 @@ def run_server(
|
|
|
400
794
|
>>> run_server(transport="http", host="0.0.0.0", port=8080)
|
|
401
795
|
"""
|
|
402
796
|
if transport == "http":
|
|
797
|
+
logger.info("Starting MCP server (http) host=%s port=%s", host, port)
|
|
403
798
|
mcp.run(transport="http", host=host, port=port)
|
|
404
799
|
else:
|
|
800
|
+
logger.info("Starting MCP server (stdio)")
|
|
405
801
|
mcp.run()
|
|
406
802
|
|
|
407
803
|
|
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
|
|
|
@@ -283,7 +283,7 @@ class OpenAIEmbeddings(EmbeddingProvider):
|
|
|
283
283
|
"https://api.openai.com/v1/models",
|
|
284
284
|
headers={"Authorization": f"Bearer {self._api_key}"},
|
|
285
285
|
)
|
|
286
|
-
return response.status_code == 200
|
|
286
|
+
return bool(response.status_code == 200)
|
|
287
287
|
|
|
288
288
|
except httpx.RequestError as e:
|
|
289
289
|
logger.debug(f"OpenAI availability check failed: {e}")
|
|
@@ -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:
|