@heytherevibin/skillforge 0.2.1 → 0.8.0

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.
@@ -0,0 +1,121 @@
1
+ """Shared MCP response shape for ``route_skills`` (and CLI parity).
2
+
3
+ Versioned ``_meta`` with ``sources[]`` and ``budget``. Schema **1.1** adds
4
+ per-chunk ``sources`` when ``context_items`` (RAG chunks) are returned from Phase 1.
5
+ Schema **1.2** adds ``kind: file`` sources and project chunk char counts in ``budget``.
6
+ Schema **1.4** adds optional ``context_redaction`` (hit counts when scrubbing is on).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import Any, Mapping, Protocol
12
+
13
+ from app.redaction import redaction_enabled, redact_display_path
14
+
15
+
16
+ class _SkillBody(Protocol):
17
+ name: str
18
+ body: str
19
+
20
+
21
+ MCP_RESPONSE_SCHEMA_VERSION = "1.4"
22
+
23
+
24
+ def build_route_skills_meta(
25
+ *,
26
+ result: dict[str, Any],
27
+ picked_names: list[str],
28
+ user_id: str,
29
+ db_path: Path | str,
30
+ skills_map: Mapping[str, _SkillBody],
31
+ response_text: str,
32
+ error: str | None = None,
33
+ context_items: list[dict[str, Any]] | None = None,
34
+ fusion: dict[str, Any] | None = None,
35
+ context_redaction: dict[str, Any] | None = None,
36
+ ) -> dict[str, Any]:
37
+ """Build ``_meta`` for a route_skills-style response (success or structured error)."""
38
+ sources: list[dict[str, Any]] = []
39
+ chars_skill = 0
40
+ chars_project = 0
41
+
42
+ if context_items:
43
+ for c in context_items:
44
+ tlen = len(c.get("text") or "")
45
+ ref_path = c.get("path")
46
+ if ref_path:
47
+ row = {
48
+ "kind": "file",
49
+ "ref": ref_path,
50
+ "line_start": c.get("line_start"),
51
+ "line_end": c.get("line_end"),
52
+ "score": round(float(c.get("score", 0.0)), 6),
53
+ }
54
+ if c.get("mmr_rank") is not None:
55
+ row["mmr_rank"] = int(c["mmr_rank"])
56
+ sources.append(row)
57
+ chars_project += tlen
58
+ else:
59
+ row = {
60
+ "kind": "skill",
61
+ "ref": c["skill"],
62
+ "line_start": c.get("line_start"),
63
+ "line_end": c.get("line_end"),
64
+ "score": round(float(c.get("score", 0.0)), 6),
65
+ }
66
+ if c.get("mmr_rank") is not None:
67
+ row["mmr_rank"] = int(c["mmr_rank"])
68
+ sources.append(row)
69
+ chars_skill += tlen
70
+ else:
71
+ for n in picked_names:
72
+ s = skills_map.get(n)
73
+ if s is not None:
74
+ sources.append({
75
+ "kind": "skill",
76
+ "ref": n,
77
+ "line_start": None,
78
+ "line_end": None,
79
+ "score": None,
80
+ })
81
+ chars_skill += len(s.body)
82
+
83
+ chars_body = chars_skill + chars_project
84
+
85
+ candidates_raw = result.get("candidates") or []
86
+ candidates_preview: list[dict[str, Any]] = []
87
+ for item in candidates_raw[:15]:
88
+ if isinstance(item, tuple) and len(item) == 2:
89
+ sk, sc = item
90
+ name = getattr(sk, "name", None)
91
+ if name is not None:
92
+ candidates_preview.append({"name": name, "score": round(float(sc), 6)})
93
+
94
+ meta: dict[str, Any] = {
95
+ "schema_version": MCP_RESPONSE_SCHEMA_VERSION,
96
+ "sources": sources,
97
+ "budget": {
98
+ "chars_skill_bodies": chars_skill,
99
+ "chars_project_chunks": chars_project,
100
+ "chars_context_items_total": chars_body,
101
+ "chars_response_total": len(response_text),
102
+ "est_tokens_approx": max(1, len(response_text) // 4),
103
+ },
104
+ "picked": list(picked_names),
105
+ "reasoning": result.get("reasoning"),
106
+ "session_id": result.get("session_id"),
107
+ "user_id": user_id,
108
+ "rerouted": result.get("rerouted"),
109
+ "change_pct": round(float(result.get("change", 0)) * 100, 1),
110
+ "route_ms": round(float(result.get("route_ms", 0)), 1),
111
+ "orchestrator_db": redact_display_path(db_path) if redaction_enabled() else str(db_path),
112
+ "candidates_preview": candidates_preview,
113
+ "context_items_count": len(context_items or []),
114
+ }
115
+ if fusion is not None and fusion.get("enabled"):
116
+ meta["fusion"] = fusion
117
+ if context_redaction is not None:
118
+ meta["context_redaction"] = context_redaction
119
+ if error:
120
+ meta["error"] = error
121
+ return meta
@@ -2,11 +2,11 @@
2
2
  MCP server for skillforge.
3
3
 
4
4
  Exposes skill routing as MCP tools so MCP-aware clients (Claude Desktop,
5
- Claude Code, Cursor, etc.) can use the orchestrator without running the
6
- HTTP server.
5
+ Claude Code, Cursor, etc.) can use the orchestrator locally.
7
6
 
8
7
  Tools exposed:
9
8
  route_skills / skillforge_bootstrap — routing (+ optional project materialize).
9
+ search_skills / explain_route / get_skill — retrieval, debugging, deterministic fetch.
10
10
  materialize_project — .cursor/rules, docs/SKILLFORGE-PRD.md, CLAUDE.md block.
11
11
  list_skills, skill_feedback, skill_referenced, disable_skill.
12
12
 
@@ -25,7 +25,10 @@ from pathlib import Path
25
25
 
26
26
  from app.db_paths import resolve_orchestrator_db
27
27
  from app.main import (
28
+ TOP_K_CANDIDATES,
29
+ MAX_ACTIVE_SKILLS,
28
30
  build_router_and_skills,
31
+ format_context_items_markdown,
29
32
  init_db,
30
33
  load_all_skills,
31
34
  log_event,
@@ -36,6 +39,10 @@ from app.main import (
36
39
  Router,
37
40
  )
38
41
  from app.materialize import materialize_project_files
42
+ from app.mcp_contract import MCP_RESPONSE_SCHEMA_VERSION, build_route_skills_meta
43
+ from app.redaction import redaction_enabled, redact_display_path
44
+ from app.route_policies import load_route_policies_config, merge_policy_includes
45
+ from app.routing_signals import build_route_query_text
39
46
 
40
47
 
41
48
  def _env_truthy(name: str, default: str = "1") -> bool:
@@ -82,7 +89,7 @@ class MCPServer:
82
89
  self._db_cache: dict[str, sqlite3.Connection] = {}
83
90
 
84
91
  def _mcp_user_id(self, args: dict) -> str:
85
- """Per-tool user namespace for weights/sessions/events (aligned with HTTP bearer user id)."""
92
+ """Per-tool user namespace for weights/sessions/events."""
86
93
  raw = (
87
94
  args.get("user_id")
88
95
  or os.getenv("SKILLFORGE_MCP_USER_ID", "")
@@ -97,6 +104,15 @@ class MCPServer:
97
104
  env = os.getenv("SKILLFORGE_PROJECT_ROOT", "").strip()
98
105
  return env or None
99
106
 
107
+ @staticmethod
108
+ def _include_project_rag_from_args(args: dict) -> bool:
109
+ v = args.get("include_project_rag")
110
+ if v is True:
111
+ return True
112
+ if isinstance(v, str) and v.strip().lower() in ("1", "true", "yes"):
113
+ return True
114
+ return False
115
+
100
116
  def _get_con(self, args: dict):
101
117
  path = resolve_orchestrator_db(self._project_root_from_args(args))
102
118
  key = str(path)
@@ -173,7 +189,7 @@ class MCPServer:
173
189
  return {
174
190
  "protocolVersion": "2024-11-05",
175
191
  "capabilities": caps,
176
- "serverInfo": {"name": "skillforge", "version": "0.2.1"},
192
+ "serverInfo": {"name": "skillforge", "version": "0.7.1"},
177
193
  }
178
194
 
179
195
  def handle_tools_list(self, params):
@@ -183,11 +199,14 @@ class MCPServer:
183
199
  "name": "route_skills",
184
200
  "description": (
185
201
  "Route the user's prompt to the most relevant skills from the catalog "
186
- "and return their full SKILL.md bodies. The client should inject the "
187
- "returned content into the LLM's context. Returns up to 7 skills. "
188
- "Pass project_root (workspace path) for per-repo SQLite in .skillforge/ "
189
- "and learning; else use env SKILLFORGE_PROJECT_ROOT or global data dir. "
190
- "Optional session_id for reroute stats; optional user_id for multi-user."
202
+ "and return SKILL.md context (full body or RAG chunks per CONTEXT_MODE). "
203
+ "Returns up to 7 skills. "
204
+ "Pass project_root for per-repo SQLite in .skillforge/ and learning. "
205
+ "Optional include_project_rag merges top chunks from `skillforge index` into context. "
206
+ "On success, _meta includes schema_version ("
207
+ f"{MCP_RESPONSE_SCHEMA_VERSION}), sources[] (kind skill or file), "
208
+ "budget (chars_skill_bodies, chars_project_chunks), fusion (MMR when combined index+RAG), "
209
+ "candidates_preview, context_items_count."
191
210
  ),
192
211
  "inputSchema": {
193
212
  "type": "object",
@@ -197,6 +216,14 @@ class MCPServer:
197
216
  "type": "string",
198
217
  "description": "Repo/workspace root — stores orchestrator state in .skillforge/",
199
218
  },
219
+ "include_project_rag": {
220
+ "type": "boolean",
221
+ "description": (
222
+ "If true, append top chunks from the project index in the same DB "
223
+ "(see skillforge index). Requires project_root."
224
+ ),
225
+ "default": False,
226
+ },
200
227
  "conversation": {
201
228
  "type": "array",
202
229
  "description": "Optional recent messages for context",
@@ -208,12 +235,80 @@ class MCPServer:
208
235
  },
209
236
  "user_id": {
210
237
  "type": "string",
211
- "description": "Logical user id for weights/sessions/events (same as HTTP user id string)",
238
+ "description": "Logical user id for weights/sessions/events",
212
239
  },
213
240
  },
214
241
  "required": ["prompt"],
215
242
  },
216
243
  },
244
+ {
245
+ "name": "search_skills",
246
+ "description": (
247
+ "Embedding-only retrieval: top skills for a query with similarity scores "
248
+ "and descriptions (no Haiku, no full route). Use to explore the catalog."
249
+ ),
250
+ "inputSchema": {
251
+ "type": "object",
252
+ "properties": {
253
+ "query": {"type": "string", "description": "Search query or task text"},
254
+ "limit": {
255
+ "type": "integer",
256
+ "description": f"Max skills to return (default {TOP_K_CANDIDATES})",
257
+ },
258
+ "project_root": {"type": "string"},
259
+ "user_id": {"type": "string"},
260
+ },
261
+ "required": ["query"],
262
+ },
263
+ },
264
+ {
265
+ "name": "explain_route",
266
+ "description": (
267
+ "Debug routing: embedding facets for the shortlist (same query text as route_skills when "
268
+ "`conversation` is passed — conversation-aware when SKILLFORGE_ROUTER_CONV_MAX_TURNS > 0), "
269
+ "optional Haiku rerank, Haiku/embedding-only pick with reasoning, and policy merge audit. "
270
+ "Does not write sessions or increment uses."
271
+ ),
272
+ "inputSchema": {
273
+ "type": "object",
274
+ "properties": {
275
+ "prompt": {"type": "string"},
276
+ "conversation": {"type": "array", "items": {"type": "object"}},
277
+ "limit": {
278
+ "type": "integer",
279
+ "description": "Max shortlist rows in facets (default TOP_K)",
280
+ },
281
+ "project_root": {"type": "string"},
282
+ "user_id": {"type": "string"},
283
+ },
284
+ "required": ["prompt"],
285
+ },
286
+ },
287
+ {
288
+ "name": "get_skill",
289
+ "description": (
290
+ "Load one skill by name: full SKILL.md body or a short summary. "
291
+ "Use for deterministic workflows when you already know the skill name."
292
+ ),
293
+ "inputSchema": {
294
+ "type": "object",
295
+ "properties": {
296
+ "skill_name": {"type": "string"},
297
+ "format": {
298
+ "type": "string",
299
+ "enum": ["full", "summary"],
300
+ "description": "summary = description + first ~8k chars of body",
301
+ "default": "full",
302
+ },
303
+ "max_chars": {
304
+ "type": "integer",
305
+ "description": "If > 0, truncate body to this many characters",
306
+ "default": 0,
307
+ },
308
+ },
309
+ "required": ["skill_name"],
310
+ },
311
+ },
217
312
  {
218
313
  "name": "list_skills",
219
314
  "description": (
@@ -317,6 +412,11 @@ class MCPServer:
317
412
  "session_id": {"type": "string"},
318
413
  "user_id": {"type": "string"},
319
414
  "merge": {"type": "boolean", "default": True},
415
+ "include_project_rag": {
416
+ "type": "boolean",
417
+ "description": "Same as route_skills: merge indexed project file chunks into context.",
418
+ "default": False,
419
+ },
320
420
  },
321
421
  "required": ["prompt", "project_root"],
322
422
  },
@@ -330,6 +430,12 @@ class MCPServer:
330
430
 
331
431
  if name == "route_skills":
332
432
  return await self._tool_route_skills(args)
433
+ if name == "search_skills":
434
+ return self._tool_search_skills(args)
435
+ if name == "explain_route":
436
+ return await self._tool_explain_route(args)
437
+ if name == "get_skill":
438
+ return self._tool_get_skill(args)
333
439
  if name == "list_skills":
334
440
  return self._tool_list_skills(args)
335
441
  if name == "skill_feedback":
@@ -349,8 +455,25 @@ class MCPServer:
349
455
  conversation = args.get("conversation", [])
350
456
  session_id = args.get("session_id") or None
351
457
  user_id = self._mcp_user_id(args)
458
+ pr = self._project_root_from_args(args)
459
+ db_path = resolve_orchestrator_db(pr)
460
+
352
461
  if not prompt.strip():
353
- return {"content": [{"type": "text", "text": "No prompt provided."}]}
462
+ err_text = "No prompt provided."
463
+ return {
464
+ "content": [{"type": "text", "text": err_text}],
465
+ "isError": True,
466
+ "_meta": build_route_skills_meta(
467
+ result={"candidates": []},
468
+ picked_names=[],
469
+ user_id=user_id,
470
+ db_path=db_path,
471
+ skills_map=self.skills or {},
472
+ response_text=err_text,
473
+ error="empty_prompt",
474
+ ),
475
+ }
476
+
354
477
  con = self._get_con(args)
355
478
  result = await run_route_turn(
356
479
  con,
@@ -359,11 +482,12 @@ class MCPServer:
359
482
  conversation,
360
483
  user_id=user_id,
361
484
  session_id=session_id,
485
+ project_root=pr,
486
+ include_project_rag=self._include_project_rag_from_args(args),
362
487
  )
363
488
  picked_names = result["picked_names"]
364
489
  reasoning = result["reasoning"]
365
- pr = self._project_root_from_args(args)
366
- db_path = resolve_orchestrator_db(pr)
490
+ context_items = result.get("context_items") or []
367
491
  if pr:
368
492
  try:
369
493
  d = Path(pr).expanduser().resolve() / ".skillforge"
@@ -375,35 +499,184 @@ class MCPServer:
375
499
  "reasoning": reasoning,
376
500
  "route_ms": round(result["route_ms"], 1),
377
501
  "user_id": user_id,
502
+ "schema_version": MCP_RESPONSE_SCHEMA_VERSION,
503
+ "context_mode": self.router.context_mode,
504
+ "context_items_count": len(context_items),
505
+ "project_rag_items_count": (result.get("event") or {}).get("project_rag_items_count", 0),
378
506
  }
379
507
  (d / "last_route.json").write_text(json.dumps(snap, indent=2), encoding="utf-8")
380
508
  except OSError:
381
509
  pass
382
510
 
383
- # Build response: a header explaining what was loaded, then the skill bodies
511
+ db_disp = redact_display_path(db_path) if redaction_enabled() else str(db_path)
384
512
  blocks = [
385
- f"# Skillforge — routed {len(picked_names)} skill(s)",
386
- f"_DB:_ `{db_path}`",
513
+ f"# Skillforge — routed {len(picked_names)} skill(s); context=`{self.router.context_mode}`",
514
+ f"_DB:_ `{db_disp}`",
387
515
  f"_Reasoning: {reasoning}_" if reasoning else "",
388
516
  "",
389
517
  ]
390
- for n in picked_names:
391
- s = self.skills.get(n)
392
- if s:
393
- blocks.append(f"---\n## Skill: {s.name}\n\n{s.body}\n")
394
- if not picked_names:
518
+ if context_items:
519
+ blocks.append(format_context_items_markdown(context_items))
520
+ elif not picked_names:
395
521
  blocks.append("_No skills matched this prompt closely enough to load._")
522
+ response_text = "\n".join(b for b in blocks if b is not None)
523
+ meta = build_route_skills_meta(
524
+ result=result,
525
+ picked_names=picked_names,
526
+ user_id=user_id,
527
+ db_path=db_path,
528
+ skills_map=self.skills,
529
+ response_text=response_text,
530
+ context_items=context_items,
531
+ fusion=(result.get("event") or {}).get("context_fusion"),
532
+ context_redaction=(result.get("event") or {}).get("context_redaction"),
533
+ )
396
534
  return {
397
- "content": [{"type": "text", "text": "\n".join(b for b in blocks if b is not None)}],
535
+ "content": [{"type": "text", "text": response_text}],
536
+ "_meta": meta,
537
+ }
538
+
539
+ def _tool_search_skills(self, args):
540
+ query = (args.get("query") or "").strip()
541
+ user_id = self._mcp_user_id(args)
542
+ pr = self._project_root_from_args(args)
543
+ db_path = resolve_orchestrator_db(pr)
544
+ if not query:
545
+ return {
546
+ "content": [{"type": "text", "text": "query is required."}],
547
+ "isError": True,
548
+ }
549
+ try:
550
+ limit = int(args.get("limit") or TOP_K_CANDIDATES)
551
+ except (TypeError, ValueError):
552
+ limit = TOP_K_CANDIDATES
553
+ limit = max(1, min(limit, 50))
554
+ con = self._get_con(args)
555
+ facets = self.router.shortlist_with_facets(query, con, k=limit, user_id=user_id)
556
+ lines = ["# search_skills — embedding shortlist", ""]
557
+ for f in facets:
558
+ lines.append(
559
+ f"- **{f['name']}** (cos {f['cosine_similarity']}, score {f['routing_score']}): "
560
+ f"{(f.get('description_preview') or '')[:220]}"
561
+ )
562
+ text = "\n".join(lines)
563
+ return {
564
+ "content": [{"type": "text", "text": text}],
398
565
  "_meta": {
399
- "picked": picked_names,
400
- "reasoning": reasoning,
401
- "session_id": result["session_id"],
402
- "user_id": user_id,
403
- "rerouted": result["rerouted"],
404
- "change_pct": round(result["change"] * 100, 1),
405
- "route_ms": round(result["route_ms"], 1),
406
- "orchestrator_db": str(db_path),
566
+ "schema_version": MCP_RESPONSE_SCHEMA_VERSION,
567
+ "tool": "search_skills",
568
+ "orchestrator_db": redact_display_path(db_path) if redaction_enabled() else str(db_path),
569
+ "results": facets,
570
+ "count": len(facets),
571
+ },
572
+ }
573
+
574
+ async def _tool_explain_route(self, args):
575
+ prompt = (args.get("prompt") or "").strip()
576
+ conversation = args.get("conversation") or []
577
+ user_id = self._mcp_user_id(args)
578
+ pr = self._project_root_from_args(args)
579
+ db_path = resolve_orchestrator_db(pr)
580
+ if not prompt:
581
+ return {
582
+ "content": [{"type": "text", "text": "prompt is required."}],
583
+ "isError": True,
584
+ }
585
+ try:
586
+ limit = int(args.get("limit") or TOP_K_CANDIDATES)
587
+ except (TypeError, ValueError):
588
+ limit = TOP_K_CANDIDATES
589
+ limit = max(1, min(limit, 50))
590
+ con = self._get_con(args)
591
+ route_query = build_route_query_text(prompt, conversation)
592
+ facets = self.router.shortlist_with_facets(route_query, con, k=limit, user_id=user_id)
593
+ candidates = self.router.shortlist(route_query, con, user_id=user_id)
594
+ candidates = await self.router.rerank_candidates_haiku(route_query, conversation, candidates)
595
+ picked, reasoning = await self.router.pick_final(
596
+ prompt, conversation, candidates, route_query=route_query
597
+ )
598
+ policies_cfg = load_route_policies_config(pr)
599
+ merged, policy_audit = merge_policy_includes(
600
+ prompt,
601
+ list(picked),
602
+ policies_cfg,
603
+ self.router._by_name,
604
+ con,
605
+ user_id,
606
+ max_active=MAX_ACTIVE_SKILLS,
607
+ )
608
+ router_mode = "full" if self.router.anthropic else "embedding-only"
609
+ explain = {
610
+ "schema_version": MCP_RESPONSE_SCHEMA_VERSION,
611
+ "tool": "explain_route",
612
+ "orchestrator_db": redact_display_path(db_path) if redaction_enabled() else str(db_path),
613
+ "router_mode": router_mode,
614
+ "embedding_shortlist": facets,
615
+ "picked_before_policy": list(picked),
616
+ "picked_after_policy": merged,
617
+ "router_reasoning": reasoning,
618
+ "policy": {
619
+ "rules_loaded": len(policies_cfg.get("rules") or [])
620
+ if isinstance(policies_cfg.get("rules"), list)
621
+ else 0,
622
+ "audit": policy_audit,
623
+ },
624
+ }
625
+ lines = [
626
+ "# explain_route — routing diagnostics (no DB writes)",
627
+ "",
628
+ f"**Router:** {router_mode}",
629
+ f"**Picked (router):** {', '.join(picked) if picked else '_(none)_'}",
630
+ f"**After policies:** {', '.join(merged) if merged else '_(none)_'}",
631
+ f"**Reasoning:** {reasoning}" if reasoning else "**Reasoning:** _(n/a)_",
632
+ "",
633
+ "## Shortlist (embedding)",
634
+ ]
635
+ for f in facets[:15]:
636
+ lines.append(
637
+ f"- `{f['name']}` cos={f['cosine_similarity']} weight={f['learned_weight']} "
638
+ f"score={f['routing_score']}"
639
+ )
640
+ if policy_audit:
641
+ lines.extend(["", "## Policy audit"])
642
+ for row in policy_audit[:30]:
643
+ lines.append(f"- {row}")
644
+ body = "\n".join(lines)
645
+ return {"content": [{"type": "text", "text": body}], "_meta": explain}
646
+
647
+ def _tool_get_skill(self, args):
648
+ name = (args.get("skill_name") or "").strip()
649
+ fmt = (args.get("format") or "full").strip().lower()
650
+ if fmt not in ("full", "summary"):
651
+ fmt = "full"
652
+ max_chars = args.get("max_chars")
653
+ try:
654
+ mc = int(max_chars) if max_chars is not None else 0
655
+ except (TypeError, ValueError):
656
+ mc = 0
657
+ if not name or name not in self.skills:
658
+ return {
659
+ "content": [{"type": "text", "text": f"Unknown skill: {name or '(empty)'}"}],
660
+ "isError": True,
661
+ }
662
+ s = self.skills[name]
663
+ if fmt == "summary":
664
+ body = f"{s.description}\n\n---\n\n{(s.body or '')[:8000]}"
665
+ else:
666
+ body = s.body or ""
667
+ if mc > 0:
668
+ body = body[:mc]
669
+ header = f"# get_skill: `{name}`\n**Source:** {s.source} · **format:** {fmt}\n\n"
670
+ text = header + body
671
+ return {
672
+ "content": [{"type": "text", "text": text}],
673
+ "_meta": {
674
+ "schema_version": MCP_RESPONSE_SCHEMA_VERSION,
675
+ "tool": "get_skill",
676
+ "skill_name": name,
677
+ "source": s.source,
678
+ "format": fmt,
679
+ "chars": len(body),
407
680
  },
408
681
  }
409
682
 
@@ -514,6 +787,7 @@ class MCPServer:
514
787
  "conversation": conversation,
515
788
  "session_id": session_id,
516
789
  "user_id": user_id,
790
+ "include_project_rag": self._include_project_rag_from_args(args),
517
791
  }
518
792
  )
519
793
  if route.get("isError"):