alita-sdk 0.3.379__py3-none-any.whl → 0.3.627__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 (278) hide show
  1. alita_sdk/cli/__init__.py +10 -0
  2. alita_sdk/cli/__main__.py +17 -0
  3. alita_sdk/cli/agent/__init__.py +5 -0
  4. alita_sdk/cli/agent/default.py +258 -0
  5. alita_sdk/cli/agent_executor.py +156 -0
  6. alita_sdk/cli/agent_loader.py +245 -0
  7. alita_sdk/cli/agent_ui.py +228 -0
  8. alita_sdk/cli/agents.py +3113 -0
  9. alita_sdk/cli/callbacks.py +647 -0
  10. alita_sdk/cli/cli.py +168 -0
  11. alita_sdk/cli/config.py +306 -0
  12. alita_sdk/cli/context/__init__.py +30 -0
  13. alita_sdk/cli/context/cleanup.py +198 -0
  14. alita_sdk/cli/context/manager.py +731 -0
  15. alita_sdk/cli/context/message.py +285 -0
  16. alita_sdk/cli/context/strategies.py +289 -0
  17. alita_sdk/cli/context/token_estimation.py +127 -0
  18. alita_sdk/cli/formatting.py +182 -0
  19. alita_sdk/cli/input_handler.py +419 -0
  20. alita_sdk/cli/inventory.py +1073 -0
  21. alita_sdk/cli/mcp_loader.py +315 -0
  22. alita_sdk/cli/testcases/__init__.py +94 -0
  23. alita_sdk/cli/testcases/data_generation.py +119 -0
  24. alita_sdk/cli/testcases/discovery.py +96 -0
  25. alita_sdk/cli/testcases/executor.py +84 -0
  26. alita_sdk/cli/testcases/logger.py +85 -0
  27. alita_sdk/cli/testcases/parser.py +172 -0
  28. alita_sdk/cli/testcases/prompts.py +91 -0
  29. alita_sdk/cli/testcases/reporting.py +125 -0
  30. alita_sdk/cli/testcases/setup.py +108 -0
  31. alita_sdk/cli/testcases/test_runner.py +282 -0
  32. alita_sdk/cli/testcases/utils.py +39 -0
  33. alita_sdk/cli/testcases/validation.py +90 -0
  34. alita_sdk/cli/testcases/workflow.py +196 -0
  35. alita_sdk/cli/toolkit.py +327 -0
  36. alita_sdk/cli/toolkit_loader.py +85 -0
  37. alita_sdk/cli/tools/__init__.py +43 -0
  38. alita_sdk/cli/tools/approval.py +224 -0
  39. alita_sdk/cli/tools/filesystem.py +1751 -0
  40. alita_sdk/cli/tools/planning.py +389 -0
  41. alita_sdk/cli/tools/terminal.py +414 -0
  42. alita_sdk/community/__init__.py +72 -12
  43. alita_sdk/community/inventory/__init__.py +236 -0
  44. alita_sdk/community/inventory/config.py +257 -0
  45. alita_sdk/community/inventory/enrichment.py +2137 -0
  46. alita_sdk/community/inventory/extractors.py +1469 -0
  47. alita_sdk/community/inventory/ingestion.py +3172 -0
  48. alita_sdk/community/inventory/knowledge_graph.py +1457 -0
  49. alita_sdk/community/inventory/parsers/__init__.py +218 -0
  50. alita_sdk/community/inventory/parsers/base.py +295 -0
  51. alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
  52. alita_sdk/community/inventory/parsers/go_parser.py +851 -0
  53. alita_sdk/community/inventory/parsers/html_parser.py +389 -0
  54. alita_sdk/community/inventory/parsers/java_parser.py +593 -0
  55. alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
  56. alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
  57. alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
  58. alita_sdk/community/inventory/parsers/python_parser.py +604 -0
  59. alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
  60. alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
  61. alita_sdk/community/inventory/parsers/text_parser.py +322 -0
  62. alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
  63. alita_sdk/community/inventory/patterns/__init__.py +61 -0
  64. alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
  65. alita_sdk/community/inventory/patterns/loader.py +348 -0
  66. alita_sdk/community/inventory/patterns/registry.py +198 -0
  67. alita_sdk/community/inventory/presets.py +535 -0
  68. alita_sdk/community/inventory/retrieval.py +1403 -0
  69. alita_sdk/community/inventory/toolkit.py +173 -0
  70. alita_sdk/community/inventory/toolkit_utils.py +176 -0
  71. alita_sdk/community/inventory/visualize.py +1370 -0
  72. alita_sdk/configurations/__init__.py +1 -1
  73. alita_sdk/configurations/ado.py +141 -20
  74. alita_sdk/configurations/bitbucket.py +94 -2
  75. alita_sdk/configurations/confluence.py +130 -1
  76. alita_sdk/configurations/figma.py +76 -0
  77. alita_sdk/configurations/gitlab.py +91 -0
  78. alita_sdk/configurations/jira.py +103 -0
  79. alita_sdk/configurations/openapi.py +329 -0
  80. alita_sdk/configurations/qtest.py +72 -1
  81. alita_sdk/configurations/report_portal.py +96 -0
  82. alita_sdk/configurations/sharepoint.py +148 -0
  83. alita_sdk/configurations/testio.py +83 -0
  84. alita_sdk/configurations/testrail.py +88 -0
  85. alita_sdk/configurations/xray.py +93 -0
  86. alita_sdk/configurations/zephyr_enterprise.py +93 -0
  87. alita_sdk/configurations/zephyr_essential.py +75 -0
  88. alita_sdk/runtime/clients/artifact.py +3 -3
  89. alita_sdk/runtime/clients/client.py +388 -46
  90. alita_sdk/runtime/clients/mcp_discovery.py +342 -0
  91. alita_sdk/runtime/clients/mcp_manager.py +262 -0
  92. alita_sdk/runtime/clients/sandbox_client.py +8 -21
  93. alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
  94. alita_sdk/runtime/langchain/assistant.py +157 -39
  95. alita_sdk/runtime/langchain/constants.py +647 -1
  96. alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
  97. alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +103 -60
  98. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
  99. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +10 -4
  100. alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +226 -7
  101. alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +5 -2
  102. alita_sdk/runtime/langchain/document_loaders/constants.py +40 -19
  103. alita_sdk/runtime/langchain/langraph_agent.py +405 -84
  104. alita_sdk/runtime/langchain/utils.py +106 -7
  105. alita_sdk/runtime/llms/preloaded.py +2 -6
  106. alita_sdk/runtime/models/mcp_models.py +61 -0
  107. alita_sdk/runtime/skills/__init__.py +91 -0
  108. alita_sdk/runtime/skills/callbacks.py +498 -0
  109. alita_sdk/runtime/skills/discovery.py +540 -0
  110. alita_sdk/runtime/skills/executor.py +610 -0
  111. alita_sdk/runtime/skills/input_builder.py +371 -0
  112. alita_sdk/runtime/skills/models.py +330 -0
  113. alita_sdk/runtime/skills/registry.py +355 -0
  114. alita_sdk/runtime/skills/skill_runner.py +330 -0
  115. alita_sdk/runtime/toolkits/__init__.py +31 -0
  116. alita_sdk/runtime/toolkits/application.py +29 -10
  117. alita_sdk/runtime/toolkits/artifact.py +20 -11
  118. alita_sdk/runtime/toolkits/datasource.py +13 -6
  119. alita_sdk/runtime/toolkits/mcp.py +783 -0
  120. alita_sdk/runtime/toolkits/mcp_config.py +1048 -0
  121. alita_sdk/runtime/toolkits/planning.py +178 -0
  122. alita_sdk/runtime/toolkits/skill_router.py +238 -0
  123. alita_sdk/runtime/toolkits/subgraph.py +251 -6
  124. alita_sdk/runtime/toolkits/tools.py +356 -69
  125. alita_sdk/runtime/toolkits/vectorstore.py +11 -5
  126. alita_sdk/runtime/tools/__init__.py +10 -3
  127. alita_sdk/runtime/tools/application.py +27 -6
  128. alita_sdk/runtime/tools/artifact.py +511 -28
  129. alita_sdk/runtime/tools/data_analysis.py +183 -0
  130. alita_sdk/runtime/tools/function.py +67 -35
  131. alita_sdk/runtime/tools/graph.py +10 -4
  132. alita_sdk/runtime/tools/image_generation.py +148 -46
  133. alita_sdk/runtime/tools/llm.py +1003 -128
  134. alita_sdk/runtime/tools/loop.py +3 -1
  135. alita_sdk/runtime/tools/loop_output.py +3 -1
  136. alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
  137. alita_sdk/runtime/tools/mcp_remote_tool.py +181 -0
  138. alita_sdk/runtime/tools/mcp_server_tool.py +8 -5
  139. alita_sdk/runtime/tools/planning/__init__.py +36 -0
  140. alita_sdk/runtime/tools/planning/models.py +246 -0
  141. alita_sdk/runtime/tools/planning/wrapper.py +607 -0
  142. alita_sdk/runtime/tools/router.py +2 -4
  143. alita_sdk/runtime/tools/sandbox.py +65 -48
  144. alita_sdk/runtime/tools/skill_router.py +776 -0
  145. alita_sdk/runtime/tools/tool.py +3 -1
  146. alita_sdk/runtime/tools/vectorstore.py +9 -3
  147. alita_sdk/runtime/tools/vectorstore_base.py +70 -14
  148. alita_sdk/runtime/utils/AlitaCallback.py +137 -21
  149. alita_sdk/runtime/utils/constants.py +5 -1
  150. alita_sdk/runtime/utils/mcp_client.py +492 -0
  151. alita_sdk/runtime/utils/mcp_oauth.py +361 -0
  152. alita_sdk/runtime/utils/mcp_sse_client.py +434 -0
  153. alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
  154. alita_sdk/runtime/utils/serialization.py +155 -0
  155. alita_sdk/runtime/utils/streamlit.py +40 -13
  156. alita_sdk/runtime/utils/toolkit_utils.py +30 -9
  157. alita_sdk/runtime/utils/utils.py +36 -0
  158. alita_sdk/tools/__init__.py +134 -35
  159. alita_sdk/tools/ado/repos/__init__.py +51 -32
  160. alita_sdk/tools/ado/repos/repos_wrapper.py +148 -89
  161. alita_sdk/tools/ado/test_plan/__init__.py +25 -9
  162. alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +23 -1
  163. alita_sdk/tools/ado/utils.py +1 -18
  164. alita_sdk/tools/ado/wiki/__init__.py +25 -12
  165. alita_sdk/tools/ado/wiki/ado_wrapper.py +291 -22
  166. alita_sdk/tools/ado/work_item/__init__.py +26 -13
  167. alita_sdk/tools/ado/work_item/ado_wrapper.py +73 -11
  168. alita_sdk/tools/advanced_jira_mining/__init__.py +11 -8
  169. alita_sdk/tools/aws/delta_lake/__init__.py +13 -9
  170. alita_sdk/tools/aws/delta_lake/tool.py +5 -1
  171. alita_sdk/tools/azure_ai/search/__init__.py +11 -8
  172. alita_sdk/tools/azure_ai/search/api_wrapper.py +1 -1
  173. alita_sdk/tools/base/tool.py +5 -1
  174. alita_sdk/tools/base_indexer_toolkit.py +271 -84
  175. alita_sdk/tools/bitbucket/__init__.py +17 -11
  176. alita_sdk/tools/bitbucket/api_wrapper.py +59 -11
  177. alita_sdk/tools/bitbucket/cloud_api_wrapper.py +49 -35
  178. alita_sdk/tools/browser/__init__.py +5 -4
  179. alita_sdk/tools/carrier/__init__.py +5 -6
  180. alita_sdk/tools/carrier/backend_reports_tool.py +6 -6
  181. alita_sdk/tools/carrier/run_ui_test_tool.py +6 -6
  182. alita_sdk/tools/carrier/ui_reports_tool.py +5 -5
  183. alita_sdk/tools/chunkers/__init__.py +3 -1
  184. alita_sdk/tools/chunkers/code/treesitter/treesitter.py +37 -13
  185. alita_sdk/tools/chunkers/sematic/json_chunker.py +1 -0
  186. alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
  187. alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
  188. alita_sdk/tools/chunkers/universal_chunker.py +270 -0
  189. alita_sdk/tools/cloud/aws/__init__.py +10 -7
  190. alita_sdk/tools/cloud/azure/__init__.py +10 -7
  191. alita_sdk/tools/cloud/gcp/__init__.py +10 -7
  192. alita_sdk/tools/cloud/k8s/__init__.py +10 -7
  193. alita_sdk/tools/code/linter/__init__.py +10 -8
  194. alita_sdk/tools/code/loaders/codesearcher.py +3 -2
  195. alita_sdk/tools/code/sonar/__init__.py +11 -8
  196. alita_sdk/tools/code_indexer_toolkit.py +82 -22
  197. alita_sdk/tools/confluence/__init__.py +22 -16
  198. alita_sdk/tools/confluence/api_wrapper.py +107 -30
  199. alita_sdk/tools/confluence/loader.py +14 -2
  200. alita_sdk/tools/custom_open_api/__init__.py +12 -5
  201. alita_sdk/tools/elastic/__init__.py +11 -8
  202. alita_sdk/tools/elitea_base.py +493 -30
  203. alita_sdk/tools/figma/__init__.py +58 -11
  204. alita_sdk/tools/figma/api_wrapper.py +1235 -143
  205. alita_sdk/tools/figma/figma_client.py +73 -0
  206. alita_sdk/tools/figma/toon_tools.py +2748 -0
  207. alita_sdk/tools/github/__init__.py +14 -15
  208. alita_sdk/tools/github/github_client.py +224 -100
  209. alita_sdk/tools/github/graphql_client_wrapper.py +119 -33
  210. alita_sdk/tools/github/schemas.py +14 -5
  211. alita_sdk/tools/github/tool.py +5 -1
  212. alita_sdk/tools/github/tool_prompts.py +9 -22
  213. alita_sdk/tools/gitlab/__init__.py +16 -11
  214. alita_sdk/tools/gitlab/api_wrapper.py +218 -48
  215. alita_sdk/tools/gitlab_org/__init__.py +10 -9
  216. alita_sdk/tools/gitlab_org/api_wrapper.py +63 -64
  217. alita_sdk/tools/google/bigquery/__init__.py +13 -12
  218. alita_sdk/tools/google/bigquery/tool.py +5 -1
  219. alita_sdk/tools/google_places/__init__.py +11 -8
  220. alita_sdk/tools/google_places/api_wrapper.py +1 -1
  221. alita_sdk/tools/jira/__init__.py +17 -10
  222. alita_sdk/tools/jira/api_wrapper.py +92 -41
  223. alita_sdk/tools/keycloak/__init__.py +11 -8
  224. alita_sdk/tools/localgit/__init__.py +9 -3
  225. alita_sdk/tools/localgit/local_git.py +62 -54
  226. alita_sdk/tools/localgit/tool.py +5 -1
  227. alita_sdk/tools/memory/__init__.py +12 -4
  228. alita_sdk/tools/non_code_indexer_toolkit.py +1 -0
  229. alita_sdk/tools/ocr/__init__.py +11 -8
  230. alita_sdk/tools/openapi/__init__.py +491 -106
  231. alita_sdk/tools/openapi/api_wrapper.py +1368 -0
  232. alita_sdk/tools/openapi/tool.py +20 -0
  233. alita_sdk/tools/pandas/__init__.py +20 -12
  234. alita_sdk/tools/pandas/api_wrapper.py +38 -25
  235. alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
  236. alita_sdk/tools/postman/__init__.py +10 -9
  237. alita_sdk/tools/pptx/__init__.py +11 -10
  238. alita_sdk/tools/pptx/pptx_wrapper.py +1 -1
  239. alita_sdk/tools/qtest/__init__.py +31 -11
  240. alita_sdk/tools/qtest/api_wrapper.py +2135 -86
  241. alita_sdk/tools/rally/__init__.py +10 -9
  242. alita_sdk/tools/rally/api_wrapper.py +1 -1
  243. alita_sdk/tools/report_portal/__init__.py +12 -8
  244. alita_sdk/tools/salesforce/__init__.py +10 -8
  245. alita_sdk/tools/servicenow/__init__.py +17 -15
  246. alita_sdk/tools/servicenow/api_wrapper.py +1 -1
  247. alita_sdk/tools/sharepoint/__init__.py +10 -7
  248. alita_sdk/tools/sharepoint/api_wrapper.py +129 -38
  249. alita_sdk/tools/sharepoint/authorization_helper.py +191 -1
  250. alita_sdk/tools/sharepoint/utils.py +8 -2
  251. alita_sdk/tools/slack/__init__.py +10 -7
  252. alita_sdk/tools/slack/api_wrapper.py +2 -2
  253. alita_sdk/tools/sql/__init__.py +12 -9
  254. alita_sdk/tools/testio/__init__.py +10 -7
  255. alita_sdk/tools/testrail/__init__.py +11 -10
  256. alita_sdk/tools/testrail/api_wrapper.py +1 -1
  257. alita_sdk/tools/utils/__init__.py +9 -4
  258. alita_sdk/tools/utils/content_parser.py +103 -18
  259. alita_sdk/tools/utils/text_operations.py +410 -0
  260. alita_sdk/tools/utils/tool_prompts.py +79 -0
  261. alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +30 -13
  262. alita_sdk/tools/xray/__init__.py +13 -9
  263. alita_sdk/tools/yagmail/__init__.py +9 -3
  264. alita_sdk/tools/zephyr/__init__.py +10 -7
  265. alita_sdk/tools/zephyr_enterprise/__init__.py +11 -7
  266. alita_sdk/tools/zephyr_essential/__init__.py +10 -7
  267. alita_sdk/tools/zephyr_essential/api_wrapper.py +30 -13
  268. alita_sdk/tools/zephyr_essential/client.py +2 -2
  269. alita_sdk/tools/zephyr_scale/__init__.py +11 -8
  270. alita_sdk/tools/zephyr_scale/api_wrapper.py +2 -2
  271. alita_sdk/tools/zephyr_squad/__init__.py +10 -7
  272. {alita_sdk-0.3.379.dist-info → alita_sdk-0.3.627.dist-info}/METADATA +154 -8
  273. alita_sdk-0.3.627.dist-info/RECORD +468 -0
  274. alita_sdk-0.3.627.dist-info/entry_points.txt +2 -0
  275. alita_sdk-0.3.379.dist-info/RECORD +0 -360
  276. {alita_sdk-0.3.379.dist-info → alita_sdk-0.3.627.dist-info}/WHEEL +0 -0
  277. {alita_sdk-0.3.379.dist-info → alita_sdk-0.3.627.dist-info}/licenses/LICENSE +0 -0
  278. {alita_sdk-0.3.379.dist-info → alita_sdk-0.3.627.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1403 @@
1
+ """
2
+ Inventory Retrieval Toolkit.
3
+
4
+ A pure query toolkit for retrieving context from a pre-built knowledge graph.
5
+ This toolkit can be added to any agent to provide knowledge graph context.
6
+
7
+ This is NOT for ingestion - use IngestionPipeline for that.
8
+
9
+ Features:
10
+ - Search entities by name, type, or properties
11
+ - Get entity details with relations
12
+ - Retrieve source content via citations
13
+ - Impact analysis (upstream/downstream dependencies)
14
+ - Citation summaries
15
+
16
+ Usage:
17
+ # Add to any agent as a toolkit
18
+ retrieval = InventoryRetrievalToolkit(
19
+ graph_path="/path/to/graph.json",
20
+ base_directory="/path/to/source" # For local content retrieval
21
+ )
22
+
23
+ tools = retrieval.get_tools()
24
+
25
+ # Or use the API wrapper directly
26
+ api = InventoryRetrievalApiWrapper(
27
+ graph_path="/path/to/graph.json"
28
+ )
29
+
30
+ results = api.search_graph("UserService")
31
+ entity = api.get_entity("UserService")
32
+ content = api.get_entity_content("UserService")
33
+ """
34
+
35
+ import logging
36
+ from pathlib import Path
37
+ from typing import Any, Optional, List, Dict
38
+
39
+ from pydantic import Field, create_model, PrivateAttr
40
+
41
+ from ...tools.elitea_base import BaseToolApiWrapper
42
+ from .knowledge_graph import KnowledgeGraph
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+
47
+ # ========== Tool Parameter Schemas ==========
48
+
49
+ SearchGraphParams = create_model(
50
+ "SearchGraphParams",
51
+ query=(str, Field(description="Search query for finding entities. Supports token matching (e.g., 'chat message' finds 'ChatMessageHandler')")),
52
+ entity_type=(Optional[str], Field(default=None, description="Filter by entity type (e.g., 'class', 'function', 'method'). Case-insensitive.")),
53
+ layer=(Optional[str], Field(default=None, description="Filter by semantic layer: 'code' (classes/functions), 'service' (APIs/endpoints), 'data' (models/schemas), 'product' (features/menus), 'domain' (concepts/processes), 'documentation', 'configuration', 'testing', 'tooling', 'knowledge' (facts)")),
54
+ file_pattern=(Optional[str], Field(default=None, description="Filter by file path pattern (glob-like, e.g., '**/chat*.py', 'api/v2/*.py')")),
55
+ top_k=(Optional[int], Field(default=10, description="Number of results to return")),
56
+ )
57
+
58
+ SearchFactsParams = create_model(
59
+ "SearchFactsParams",
60
+ query=(Optional[str], Field(default=None, description="Optional search query to filter facts by subject/content")),
61
+ fact_type=(Optional[str], Field(default=None, description="Filter by fact type: 'algorithm', 'behavior', 'validation', 'dependency', 'error_handling' (code), or 'decision', 'requirement', 'definition', 'date', 'reference', 'contact' (text)")),
62
+ file_pattern=(Optional[str], Field(default=None, description="Filter by file path pattern (glob-like)")),
63
+ top_k=(Optional[int], Field(default=20, description="Maximum number of facts to return")),
64
+ )
65
+
66
+ GetEntityParams = create_model(
67
+ "GetEntityParams",
68
+ entity_name=(str, Field(description="Name of entity to retrieve")),
69
+ include_relations=(Optional[bool], Field(default=True, description="Include related entities")),
70
+ )
71
+
72
+ GetEntityContentParams = create_model(
73
+ "GetEntityContentParams",
74
+ entity_name=(str, Field(description="Name of entity to get source content for")),
75
+ )
76
+
77
+ ImpactAnalysisParams = create_model(
78
+ "ImpactAnalysisParams",
79
+ entity_name=(str, Field(description="Name of entity to analyze")),
80
+ direction=(Optional[str], Field(default="downstream", description="'downstream' (what depends on this) or 'upstream' (what this depends on)")),
81
+ max_depth=(Optional[int], Field(default=3, description="Maximum traversal depth")),
82
+ )
83
+
84
+ GetRelatedEntitiesParams = create_model(
85
+ "GetRelatedEntitiesParams",
86
+ entity_name=(str, Field(description="Name of entity")),
87
+ relation_type=(Optional[str], Field(default=None, description="Filter by relation type")),
88
+ direction=(Optional[str], Field(default="both", description="'outgoing', 'incoming', or 'both'")),
89
+ )
90
+
91
+ GetStatsParams = create_model(
92
+ "GetStatsParams",
93
+ )
94
+
95
+ GetCitationsParams = create_model(
96
+ "GetCitationsParams",
97
+ query=(Optional[str], Field(default=None, description="Search query to filter citations")),
98
+ file_path=(Optional[str], Field(default=None, description="Filter by file path")),
99
+ )
100
+
101
+ ListEntitiesByTypeParams = create_model(
102
+ "ListEntitiesByTypeParams",
103
+ entity_type=(str, Field(description="Type of entities to list (e.g., 'class', 'function', 'api_endpoint')")),
104
+ limit=(Optional[int], Field(default=50, description="Maximum number of entities to return")),
105
+ )
106
+
107
+ ListEntitiesByLayerParams = create_model(
108
+ "ListEntitiesByLayerParams",
109
+ layer=(str, Field(description="Layer to list entities from: 'code' (classes/functions/methods), 'service' (APIs/RPCs), 'data' (models/schemas), 'product' (features/UI), 'domain' (concepts/processes), 'documentation', 'configuration', 'testing', 'tooling', 'knowledge' (facts), 'structure' (files)")),
110
+ limit=(Optional[int], Field(default=50, description="Maximum number of entities to return")),
111
+ )
112
+
113
+ SearchByFileParams = create_model(
114
+ "SearchByFileParams",
115
+ file_pattern=(str, Field(description="File path pattern (glob-like, e.g., '**/chat*.py', 'api/v2/*.py', 'rpc/*.py')")),
116
+ limit=(Optional[int], Field(default=50, description="Maximum number of entities to return")),
117
+ )
118
+
119
+ GetFileInfoParams = create_model(
120
+ "GetFileInfoParams",
121
+ file_path=(str, Field(description="Path to the file to get info for (can be partial, e.g., 'utils.py' or 'src/utils.py')")),
122
+ include_entities=(Optional[bool], Field(default=True, description="Include list of entities defined in this file")),
123
+ )
124
+
125
+ ListFilesParams = create_model(
126
+ "ListFilesParams",
127
+ file_pattern=(Optional[str], Field(default=None, description="Optional file path pattern (glob-like, e.g., '**/*.py')")),
128
+ file_type=(Optional[str], Field(default=None, description="Filter by file type: 'source_file', 'document_file', 'config_file', 'web_file'")),
129
+ limit=(Optional[int], Field(default=50, description="Maximum number of files to return")),
130
+ )
131
+
132
+ AdvancedSearchParams = create_model(
133
+ "AdvancedSearchParams",
134
+ query=(Optional[str], Field(default=None, description="Text search query (optional)")),
135
+ entity_types=(Optional[str], Field(default=None, description="Comma-separated entity types to include (e.g., 'class,function,method')")),
136
+ layers=(Optional[str], Field(default=None, description="Comma-separated layers to include (e.g., 'code,service')")),
137
+ file_patterns=(Optional[str], Field(default=None, description="Comma-separated file patterns (e.g., 'api/*.py,rpc/*.py')")),
138
+ top_k=(Optional[int], Field(default=20, description="Maximum number of results")),
139
+ )
140
+
141
+
142
+ class InventoryRetrievalApiWrapper(BaseToolApiWrapper):
143
+ """
144
+ API Wrapper for Knowledge Graph Retrieval operations.
145
+
146
+ Provides tools for querying a pre-built knowledge graph.
147
+ This is a pure retrieval toolkit - no ingestion/mutation operations.
148
+
149
+ The graph stores entity metadata and citations (file paths, line ranges).
150
+ Content is retrieved on-demand from the base_directory or source toolkit.
151
+ """
152
+
153
+ # Graph persistence path (required)
154
+ graph_path: str = Field(description="Path to the knowledge graph JSON file")
155
+
156
+ # Base directory for local content retrieval (optional)
157
+ # If set, get_entity_content will read from local files
158
+ base_directory: Optional[str] = None
159
+
160
+ # Source toolkits for remote content retrieval (optional)
161
+ # Maps toolkit name -> toolkit instance for fetching content
162
+ source_toolkits: Dict[str, Any] = Field(default_factory=dict)
163
+
164
+ # Private attributes
165
+ _knowledge_graph: Optional[KnowledgeGraph] = PrivateAttr(default=None)
166
+
167
+ class Config:
168
+ arbitrary_types_allowed = True
169
+
170
+ def model_post_init(self, __context) -> None:
171
+ """Initialize after model construction."""
172
+ self._knowledge_graph = KnowledgeGraph()
173
+
174
+ # Load graph (handle model_construct case where graph_path may not be set)
175
+ graph_path = getattr(self, 'graph_path', None)
176
+ if graph_path:
177
+ try:
178
+ self._knowledge_graph.load_from_json(graph_path)
179
+ stats = self._knowledge_graph.get_stats()
180
+ logger.info(
181
+ f"Loaded graph: {stats['node_count']} entities, "
182
+ f"{stats['edge_count']} relations"
183
+ )
184
+ except FileNotFoundError:
185
+ logger.warning(f"Graph not found at {graph_path}")
186
+ except Exception as e:
187
+ logger.error(f"Failed to load graph: {e}")
188
+
189
+ def _resolve_path(self, path: str) -> Optional[Path]:
190
+ """Resolve path within base directory (if set)."""
191
+ if not self.base_directory:
192
+ return None
193
+
194
+ base = Path(self.base_directory).resolve()
195
+
196
+ if Path(path).is_absolute():
197
+ target = Path(path).resolve()
198
+ else:
199
+ target = (base / path).resolve()
200
+
201
+ # Security check
202
+ try:
203
+ target.relative_to(base)
204
+ return target
205
+ except ValueError:
206
+ logger.warning(f"Path '{path}' is outside base directory")
207
+ return None
208
+
209
+ def _read_local_file(self, path: str) -> Optional[str]:
210
+ """Read file content from base directory."""
211
+ target = self._resolve_path(path)
212
+ if target and target.exists() and target.is_file():
213
+ try:
214
+ return target.read_text(encoding='utf-8')
215
+ except Exception as e:
216
+ logger.warning(f"Failed to read {path}: {e}")
217
+ return None
218
+
219
+ def _read_local_file_lines(self, path: str, start_line: int, end_line: int) -> Optional[str]:
220
+ """Read specific lines from a local file."""
221
+ target = self._resolve_path(path)
222
+ if not target or not target.exists():
223
+ return None
224
+
225
+ try:
226
+ with open(target, 'r', encoding='utf-8') as f:
227
+ lines = f.readlines()
228
+
229
+ start_idx = max(0, start_line - 1)
230
+ end_idx = min(len(lines), end_line)
231
+
232
+ return ''.join(lines[start_idx:end_idx])
233
+ except Exception as e:
234
+ logger.warning(f"Failed to read lines from {path}: {e}")
235
+ return None
236
+
237
+ def _fetch_remote_content(
238
+ self,
239
+ source_toolkit: str,
240
+ file_path: str,
241
+ line_start: Optional[int] = None,
242
+ line_end: Optional[int] = None
243
+ ) -> Optional[str]:
244
+ """Fetch content from a source toolkit."""
245
+ if source_toolkit not in self.source_toolkits:
246
+ return None
247
+
248
+ toolkit = self.source_toolkits[source_toolkit]
249
+
250
+ try:
251
+ # Try get_files_content method (GitHub, ADO, etc.)
252
+ if hasattr(toolkit, 'get_files_content'):
253
+ content = toolkit.get_files_content(file_path)
254
+ if content and line_start:
255
+ lines = content.split('\n')
256
+ end = line_end or (line_start + 100)
257
+ return '\n'.join(lines[line_start-1:end])
258
+ return content
259
+
260
+ # Try read_file method
261
+ if hasattr(toolkit, 'read_file'):
262
+ return toolkit.read_file(file_path)
263
+
264
+ except Exception as e:
265
+ logger.warning(f"Failed to fetch from {source_toolkit}: {e}")
266
+
267
+ return None
268
+
269
+ # ========== Tool Methods ==========
270
+
271
+ def search_graph(
272
+ self,
273
+ query: str,
274
+ entity_type: Optional[str] = None,
275
+ layer: Optional[str] = None,
276
+ file_pattern: Optional[str] = None,
277
+ top_k: int = 10
278
+ ) -> str:
279
+ """
280
+ Search for entities in the knowledge graph with enhanced matching.
281
+
282
+ Supports:
283
+ - Token-based matching: "chat message" finds "ChatMessageHandler"
284
+ - File path patterns: "**/chat*.py" finds entities from chat files
285
+ - Layer filtering: "code" includes classes, functions, methods
286
+ - Type filtering: Case-insensitive matching
287
+
288
+ Returns entity metadata with citations. Use get_entity_content
289
+ to retrieve the actual source code.
290
+
291
+ Args:
292
+ query: Search query (matches entity names, descriptions, file paths)
293
+ entity_type: Optional filter by type (class, function, api_endpoint, etc.)
294
+ layer: Optional filter by layer (code, service, data, product, domain, etc.)
295
+ file_pattern: Optional glob pattern for file paths
296
+ top_k: Maximum number of results
297
+ """
298
+ self._log_tool_event(f"Searching: {query}", "search_graph")
299
+
300
+ results = self._knowledge_graph.search(
301
+ query,
302
+ top_k=top_k,
303
+ entity_type=entity_type,
304
+ layer=layer,
305
+ file_pattern=file_pattern,
306
+ )
307
+
308
+ if not results:
309
+ filters = []
310
+ if entity_type:
311
+ filters.append(f"type={entity_type}")
312
+ if layer:
313
+ filters.append(f"layer={layer}")
314
+ if file_pattern:
315
+ filters.append(f"file={file_pattern}")
316
+ filter_str = f" (filters: {', '.join(filters)})" if filters else ""
317
+ return f"No entities found matching '{query}'{filter_str}"
318
+
319
+ output = f"Found {len(results)} entities matching '{query}':\n\n"
320
+
321
+ for i, result in enumerate(results, 1):
322
+ entity = result['entity']
323
+ match_field = result.get('match_field', '')
324
+ score = result.get('score', 0)
325
+
326
+ # Get citations (support both list and legacy single citation)
327
+ citations = entity.get('citations', [])
328
+ if not citations and 'citation' in entity:
329
+ citations = [entity['citation']]
330
+
331
+ entity_type_str = entity.get('type', 'unknown')
332
+ layer_str = entity.get('layer', '')
333
+ if not layer_str:
334
+ # Infer layer from type
335
+ layer_str = self._knowledge_graph.TYPE_TO_LAYER.get(entity_type_str.lower(), '')
336
+ if layer_str:
337
+ entity_type_str = f"{layer_str}/{entity_type_str}"
338
+
339
+ output += f"{i:2}. **{entity.get('name')}** ({entity_type_str})\n"
340
+
341
+ if citations:
342
+ citation = citations[0] # Primary citation
343
+ file_path = citation.get('file_path', 'unknown')
344
+ line_info = ""
345
+ if citation.get('line_start'):
346
+ if citation.get('line_end'):
347
+ line_info = f":{citation['line_start']}-{citation['line_end']}"
348
+ else:
349
+ line_info = f":{citation['line_start']}"
350
+ output += f" 📍 `{file_path}{line_info}`\n"
351
+ elif entity.get('file_path'):
352
+ output += f" 📍 `{entity['file_path']}`\n"
353
+
354
+ # Show description if available
355
+ description = entity.get('description', '')
356
+ if not description and isinstance(entity.get('properties'), dict):
357
+ description = entity['properties'].get('description', '')
358
+ if description:
359
+ desc = description[:120]
360
+ if len(description) > 120:
361
+ desc += "..."
362
+ output += f" {desc}\n"
363
+
364
+ output += "\n"
365
+
366
+ return output
367
+
368
+ def get_entity(self, entity_name: str, include_relations: bool = True) -> str:
369
+ """
370
+ Get detailed information about a specific entity.
371
+
372
+ If multiple entities have the same name (e.g., 'Chat' as Feature vs command),
373
+ shows all matches with their types.
374
+
375
+ Returns metadata, citation, properties, and optionally relations.
376
+ Use get_entity_content for the actual source code.
377
+ """
378
+ self._log_tool_event(f"Getting entity: {entity_name}", "get_entity")
379
+
380
+ # Get all entities with this name
381
+ entities = self._knowledge_graph.find_all_entities_by_name(entity_name)
382
+
383
+ if not entities:
384
+ # Try search as fallback
385
+ results = self._knowledge_graph.search(entity_name, top_k=5)
386
+ if results:
387
+ entities = [r['entity'] for r in results]
388
+
389
+ if not entities:
390
+ return f"Entity '{entity_name}' not found"
391
+
392
+ # If multiple matches, show disambiguation
393
+ if len(entities) > 1:
394
+ output = f"# Found {len(entities)} entities named '{entity_name}'\n\n"
395
+ for i, entity in enumerate(entities, 1):
396
+ etype = entity.get('type', 'unknown')
397
+ layer = entity.get('layer', '') or self._knowledge_graph.TYPE_TO_LAYER.get(etype.lower(), '')
398
+ fp = entity.get('file_path', '')
399
+
400
+ type_str = f"{layer}/{etype}" if layer else etype
401
+ output += f"{i}. **{entity.get('name')}** ({type_str})"
402
+ if fp:
403
+ output += f" - `{fp}`"
404
+ output += f"\n ID: `{entity.get('id')}`\n\n"
405
+
406
+ output += "\n---\n\n"
407
+ output += "Showing details for the first match:\n\n"
408
+ entity = entities[0]
409
+ else:
410
+ entity = entities[0]
411
+ output = ""
412
+
413
+ # Show details for the primary entity
414
+ output += f"# {entity.get('name')}\n\n"
415
+
416
+ etype = entity.get('type', 'unknown')
417
+ layer = entity.get('layer', '') or self._knowledge_graph.TYPE_TO_LAYER.get(etype.lower(), '')
418
+
419
+ output += f"**Type:** {etype}\n"
420
+ if layer:
421
+ output += f"**Layer:** {layer}\n"
422
+
423
+ output += f"**ID:** `{entity.get('id')}`\n"
424
+
425
+ # Citations (support both list and legacy single citation)
426
+ citations = entity.get('citations', [])
427
+ if not citations and 'citation' in entity:
428
+ citations = [entity['citation']]
429
+
430
+ if citations:
431
+ output += f"\n**Locations ({len(citations)}):**\n"
432
+ for citation in citations[:5]:
433
+ if isinstance(citation, dict):
434
+ file_path = citation.get('file_path', 'unknown')
435
+ source = citation.get('source_toolkit', 'filesystem')
436
+ line_info = ""
437
+ if citation.get('line_start'):
438
+ line_info = f":{citation['line_start']}"
439
+ if citation.get('line_end'):
440
+ line_info += f"-{citation['line_end']}"
441
+ output += f"- `{file_path}{line_info}` ({source})\n"
442
+ if len(citations) > 5:
443
+ output += f"- ... and {len(citations) - 5} more citations\n"
444
+ elif entity.get('file_path'):
445
+ output += f"\n**Location:** `{entity['file_path']}`\n"
446
+
447
+ # Description
448
+ description = entity.get('description', '')
449
+ if not description and isinstance(entity.get('properties'), dict):
450
+ description = entity['properties'].get('description', '')
451
+ if description:
452
+ output += f"\n**Description:**\n{description}\n"
453
+
454
+ # Properties
455
+ skip_keys = {'id', 'name', 'type', 'layer', 'citation', 'citations', 'description', 'file_path', 'source_toolkit', 'properties'}
456
+ props = {k: v for k, v in entity.items() if k not in skip_keys}
457
+
458
+ # Also include nested properties
459
+ if isinstance(entity.get('properties'), dict):
460
+ for k, v in entity['properties'].items():
461
+ if k not in skip_keys and k != 'description':
462
+ props[k] = v
463
+
464
+ if props:
465
+ output += f"\n**Properties:**\n"
466
+ for key, value in props.items():
467
+ if isinstance(value, (list, dict)):
468
+ output += f"- {key}: {len(value)} items\n"
469
+ elif isinstance(value, str) and len(value) > 100:
470
+ output += f"- {key}: {value[:100]}...\n"
471
+ else:
472
+ output += f"- {key}: {value}\n"
473
+
474
+ # Relations
475
+ if include_relations:
476
+ entity_id = entity.get('id')
477
+ if entity_id:
478
+ relations = self._knowledge_graph.get_relations(entity_id, direction='both')
479
+ if relations:
480
+ output += f"\n**Relations ({len(relations)}):**\n"
481
+
482
+ outgoing = []
483
+ incoming = []
484
+
485
+ for rel in relations:
486
+ if rel['source'] == entity_id:
487
+ target = self._knowledge_graph.get_entity(rel['target'])
488
+ target_name = target.get('name', rel['target']) if target else rel['target']
489
+ outgoing.append(f"→ {rel['relation_type']} → **{target_name}**")
490
+ else:
491
+ source = self._knowledge_graph.get_entity(rel['source'])
492
+ source_name = source.get('name', rel['source']) if source else rel['source']
493
+ incoming.append(f"← {rel['relation_type']} ← **{source_name}**")
494
+
495
+ for r in outgoing[:5]:
496
+ output += f"- {r}\n"
497
+ if len(outgoing) > 5:
498
+ output += f"- ... and {len(outgoing) - 5} more outgoing\n"
499
+
500
+ for r in incoming[:5]:
501
+ output += f"- {r}\n"
502
+ if len(incoming) > 5:
503
+ output += f"- ... and {len(incoming) - 5} more incoming\n"
504
+
505
+ return output
506
+
507
+ def get_entity_content(self, entity_name: str) -> str:
508
+ """
509
+ Retrieve the source content for an entity using its citation.
510
+
511
+ This reads from the local filesystem or fetches from the source toolkit.
512
+ Use this when you need to see the actual source code.
513
+ """
514
+ self._log_tool_event(f"Getting content for: {entity_name}", "get_entity_content")
515
+
516
+ entity = self._knowledge_graph.find_entity_by_name(entity_name)
517
+
518
+ if not entity:
519
+ results = self._knowledge_graph.search(entity_name, top_k=1)
520
+ if results:
521
+ entity = results[0]['entity']
522
+
523
+ if not entity:
524
+ return f"Entity '{entity_name}' not found"
525
+
526
+ citation = entity.get('citation', {})
527
+ if not citation or not citation.get('file_path'):
528
+ return f"Entity '{entity_name}' has no file citation"
529
+
530
+ file_path = citation['file_path']
531
+ source_toolkit = citation.get('source_toolkit', 'filesystem')
532
+ line_start = citation.get('line_start')
533
+ line_end = citation.get('line_end')
534
+
535
+ content = None
536
+
537
+ # Try local file first
538
+ if self.base_directory:
539
+ if line_start and line_end:
540
+ content = self._read_local_file_lines(file_path, line_start, line_end)
541
+ elif line_start:
542
+ content = self._read_local_file_lines(file_path, line_start, line_start + 100)
543
+ else:
544
+ content = self._read_local_file(file_path)
545
+
546
+ # Fall back to remote fetch
547
+ if content is None and source_toolkit != 'filesystem':
548
+ content = self._fetch_remote_content(
549
+ source_toolkit, file_path, line_start, line_end
550
+ )
551
+
552
+ if content is None:
553
+ location = f"{file_path}"
554
+ if line_start:
555
+ location += f":{line_start}"
556
+ if line_end:
557
+ location += f"-{line_end}"
558
+
559
+ return (
560
+ f"Could not retrieve content for '{entity_name}'\n"
561
+ f"Location: {location}\n"
562
+ f"Source: {source_toolkit}\n\n"
563
+ f"The file may not be accessible locally. "
564
+ f"Ensure base_directory is set or the source toolkit is available."
565
+ )
566
+
567
+ # Format output
568
+ location = f"{file_path}"
569
+ if line_start:
570
+ location += f":{line_start}"
571
+ if line_end:
572
+ location += f"-{line_end}"
573
+
574
+ return f"**Source:** `{location}`\n\n```\n{content}\n```"
575
+
576
+ def impact_analysis(
577
+ self,
578
+ entity_name: str,
579
+ direction: str = "downstream",
580
+ max_depth: int = 3
581
+ ) -> str:
582
+ """
583
+ Analyze what entities would be impacted by changes.
584
+
585
+ - **downstream**: What entities depend on this one (would be affected by changes)
586
+ - **upstream**: What entities does this depend on (might cause issues here)
587
+
588
+ Useful for:
589
+ - Change impact assessment
590
+ - Dependency analysis
591
+ - Risk assessment before refactoring
592
+ """
593
+ self._log_tool_event(f"Impact analysis for: {entity_name}", "impact_analysis")
594
+
595
+ entity = self._knowledge_graph.find_entity_by_name(entity_name)
596
+
597
+ if not entity:
598
+ results = self._knowledge_graph.search(entity_name, top_k=1)
599
+ if results:
600
+ entity = results[0]['entity']
601
+
602
+ if not entity:
603
+ return f"Entity '{entity_name}' not found"
604
+
605
+ entity_id = entity.get('id')
606
+ if not entity_id:
607
+ return "Entity has no ID for analysis"
608
+
609
+ impact = self._knowledge_graph.impact_analysis(
610
+ entity_id, direction=direction, max_depth=max_depth
611
+ )
612
+
613
+ impacted = impact.get('impacted', [])
614
+
615
+ if not impacted:
616
+ return f"No {direction} dependencies found for '{entity_name}'"
617
+
618
+ output = f"# Impact Analysis: {entity_name}\n\n"
619
+ output += f"**Direction:** {direction}\n"
620
+ output += f"**Total impacted:** {len(impacted)} entities\n\n"
621
+
622
+ # Group by depth
623
+ by_depth: Dict[int, List] = {}
624
+ for item in impacted:
625
+ depth = item['depth']
626
+ if depth not in by_depth:
627
+ by_depth[depth] = []
628
+ by_depth[depth].append(item)
629
+
630
+ for depth in sorted(by_depth.keys()):
631
+ items = by_depth[depth]
632
+ output += f"## Level {depth} ({len(items)} entities)\n\n"
633
+
634
+ for item in items[:15]:
635
+ ent = item['entity']
636
+ citation = ent.get('citation', {})
637
+ location = citation.get('file_path', 'unknown') if citation else 'unknown'
638
+ output += f"- **{ent.get('name')}** ({ent.get('type')}) - `{location}`\n"
639
+
640
+ if len(items) > 15:
641
+ output += f"- ... and {len(items) - 15} more\n"
642
+
643
+ output += "\n"
644
+
645
+ return output
646
+
647
+ def get_related_entities(
648
+ self,
649
+ entity_name: str,
650
+ relation_type: Optional[str] = None,
651
+ direction: str = "both"
652
+ ) -> str:
653
+ """
654
+ Get entities related to a specific entity.
655
+
656
+ Args:
657
+ entity_name: Name of the entity
658
+ relation_type: Optional filter by relation type (CALLS, IMPORTS, EXTENDS, etc.)
659
+ direction: 'outgoing' (this → others), 'incoming' (others → this), or 'both'
660
+ """
661
+ self._log_tool_event(f"Getting related: {entity_name}", "get_related_entities")
662
+
663
+ entity = self._knowledge_graph.find_entity_by_name(entity_name)
664
+
665
+ if not entity:
666
+ results = self._knowledge_graph.search(entity_name, top_k=1)
667
+ if results:
668
+ entity = results[0]['entity']
669
+
670
+ if not entity:
671
+ return f"Entity '{entity_name}' not found"
672
+
673
+ entity_id = entity.get('id')
674
+ if not entity_id:
675
+ return "Entity has no ID"
676
+
677
+ relations = self._knowledge_graph.get_relations(entity_id, direction=direction)
678
+
679
+ # Filter by relation type if specified
680
+ if relation_type:
681
+ relations = [r for r in relations if r['relation_type'] == relation_type]
682
+
683
+ if not relations:
684
+ filter_str = f" of type '{relation_type}'" if relation_type else ""
685
+ return f"No relations{filter_str} found for '{entity_name}'"
686
+
687
+ output = f"# Related to: {entity_name}\n\n"
688
+
689
+ # Group by relation type
690
+ by_type: Dict[str, Dict[str, List]] = {}
691
+
692
+ for rel in relations:
693
+ rtype = rel['relation_type']
694
+ if rtype not in by_type:
695
+ by_type[rtype] = {'outgoing': [], 'incoming': []}
696
+
697
+ if rel['source'] == entity_id:
698
+ target = self._knowledge_graph.get_entity(rel['target'])
699
+ by_type[rtype]['outgoing'].append(target or {'name': rel['target']})
700
+ else:
701
+ source = self._knowledge_graph.get_entity(rel['source'])
702
+ by_type[rtype]['incoming'].append(source or {'name': rel['source']})
703
+
704
+ for rtype, directions in by_type.items():
705
+ output += f"## {rtype}\n\n"
706
+
707
+ if directions['outgoing']:
708
+ output += f"**Outgoing ({len(directions['outgoing'])}):**\n"
709
+ for ent in directions['outgoing'][:10]:
710
+ output += f"- → **{ent.get('name')}** ({ent.get('type', 'unknown')})\n"
711
+ if len(directions['outgoing']) > 10:
712
+ output += f"- ... and {len(directions['outgoing']) - 10} more\n"
713
+
714
+ if directions['incoming']:
715
+ output += f"**Incoming ({len(directions['incoming'])}):**\n"
716
+ for ent in directions['incoming'][:10]:
717
+ output += f"- ← **{ent.get('name')}** ({ent.get('type', 'unknown')})\n"
718
+ if len(directions['incoming']) > 10:
719
+ output += f"- ... and {len(directions['incoming']) - 10} more\n"
720
+
721
+ output += "\n"
722
+
723
+ return output
724
+
725
+ def get_stats(self) -> str:
726
+ """Get knowledge graph statistics."""
727
+ stats = self._knowledge_graph.get_stats()
728
+ schema = self._knowledge_graph.get_schema()
729
+
730
+ output = "# Knowledge Graph Statistics\n\n"
731
+ output += f"**Entities:** {stats['node_count']}\n"
732
+ output += f"**Relations:** {stats['edge_count']}\n"
733
+
734
+ if stats['entity_types']:
735
+ output += f"\n## Entity Types\n"
736
+ for etype, count in sorted(stats['entity_types'].items(), key=lambda x: -x[1]):
737
+ output += f"- {etype}: {count}\n"
738
+
739
+ if stats['relation_types']:
740
+ output += f"\n## Relation Types\n"
741
+ for rtype, count in sorted(stats['relation_types'].items(), key=lambda x: -x[1]):
742
+ output += f"- {rtype}: {count}\n"
743
+
744
+ if stats['source_toolkits']:
745
+ output += f"\n## Sources\n"
746
+ for source in stats['source_toolkits']:
747
+ output += f"- {source}\n"
748
+
749
+ if stats['last_saved']:
750
+ output += f"\n**Last updated:** {stats['last_saved']}\n"
751
+
752
+ return output
753
+
754
+ def get_citations(
755
+ self,
756
+ query: Optional[str] = None,
757
+ file_path: Optional[str] = None
758
+ ) -> str:
759
+ """
760
+ Get citations summary for entities.
761
+
762
+ Use this to see which files contain which entities.
763
+
764
+ Args:
765
+ query: Optional search query to filter entities
766
+ file_path: Optional file path to filter citations
767
+ """
768
+ self._log_tool_event(f"Getting citations (query={query}, file={file_path})", "get_citations")
769
+
770
+ if query:
771
+ citations = self._knowledge_graph.get_citations_for_query(query, top_k=20)
772
+ if not citations:
773
+ return f"No citations found for query '{query}'"
774
+
775
+ output = f"# Citations for '{query}'\n\n"
776
+ for citation in citations:
777
+ output += f"- `{citation}`\n"
778
+ return output
779
+
780
+ # Get all citations grouped by file
781
+ by_file = self._knowledge_graph.export_citations_summary()
782
+
783
+ if file_path:
784
+ # Filter to specific file
785
+ by_file = {k: v for k, v in by_file.items() if file_path in k}
786
+
787
+ if not by_file:
788
+ if file_path:
789
+ return f"No citations for file '{file_path}'"
790
+ return "No citations in graph"
791
+
792
+ output = f"# Citations ({len(by_file)} files)\n\n"
793
+
794
+ for fpath, entities in list(by_file.items())[:30]:
795
+ output += f"## `{fpath}`\n"
796
+ for ent in entities[:10]:
797
+ lines = ""
798
+ if ent.get('line_start'):
799
+ lines = f" (L{ent['line_start']}"
800
+ if ent.get('line_end'):
801
+ lines += f"-{ent['line_end']}"
802
+ lines += ")"
803
+ output += f"- **{ent['name']}** [{ent['type']}]{lines}\n"
804
+ if len(entities) > 10:
805
+ output += f"- ... and {len(entities) - 10} more\n"
806
+ output += "\n"
807
+
808
+ if len(by_file) > 30:
809
+ output += f"\n... and {len(by_file) - 30} more files\n"
810
+
811
+ return output
812
+
813
+ def list_entities_by_type(self, entity_type: str, limit: int = 50) -> str:
814
+ """
815
+ List all entities of a specific type.
816
+
817
+ Args:
818
+ entity_type: Type to filter (class, function, api_endpoint, etc.)
819
+ limit: Maximum entities to return
820
+ """
821
+ self._log_tool_event(f"Listing entities of type: {entity_type}", "list_entities_by_type")
822
+
823
+ entities = self._knowledge_graph.get_entities_by_type(entity_type, limit=limit)
824
+
825
+ if not entities:
826
+ return f"No entities of type '{entity_type}' found"
827
+
828
+ output = f"# Entities of type '{entity_type}' ({len(entities)})\n\n"
829
+
830
+ for ent in entities:
831
+ citation = ent.get('citation', {})
832
+ location = citation.get('file_path', 'unknown') if citation else 'unknown'
833
+ output += f"- **{ent.get('name')}** - `{location}`\n"
834
+
835
+ if len(entities) == limit:
836
+ output += f"\n*Limited to {limit} results*\n"
837
+
838
+ return output
839
+
840
+ def list_entities_by_layer(self, layer: str, limit: int = 50) -> str:
841
+ """
842
+ List all entities in a specific layer.
843
+
844
+ Layers:
845
+ - code: classes, functions, methods, modules
846
+ - service: API endpoints, RPC methods, handlers
847
+ - data: models, schemas, fields
848
+ - product: features, UI components, menus
849
+ - domain: concepts, processes, use cases
850
+ - documentation: guides, sections, examples
851
+ - configuration: settings, credentials
852
+ - testing: test cases, fixtures
853
+ - tooling: tools, toolkits, commands
854
+
855
+ Args:
856
+ layer: Layer to filter
857
+ limit: Maximum entities to return
858
+ """
859
+ self._log_tool_event(f"Listing entities in layer: {layer}", "list_entities_by_layer")
860
+
861
+ entities = self._knowledge_graph.get_entities_by_layer(layer, limit=limit)
862
+
863
+ if not entities:
864
+ available_layers = ", ".join(self._knowledge_graph.LAYER_TYPE_MAPPING.keys())
865
+ return f"No entities in layer '{layer}' found. Available layers: {available_layers}"
866
+
867
+ output = f"# Entities in layer '{layer}' ({len(entities)})\n\n"
868
+
869
+ # Group by type
870
+ by_type: Dict[str, List] = {}
871
+ for ent in entities:
872
+ etype = ent.get('type', 'unknown')
873
+ if etype not in by_type:
874
+ by_type[etype] = []
875
+ by_type[etype].append(ent)
876
+
877
+ for etype, ents in sorted(by_type.items(), key=lambda x: -len(x[1])):
878
+ output += f"## {etype} ({len(ents)})\n"
879
+ for ent in ents[:10]:
880
+ file_path = ent.get('file_path', '')
881
+ if file_path:
882
+ output += f"- **{ent.get('name')}** - `{file_path}`\n"
883
+ else:
884
+ output += f"- **{ent.get('name')}**\n"
885
+ if len(ents) > 10:
886
+ output += f"- ... and {len(ents) - 10} more\n"
887
+ output += "\n"
888
+
889
+ return output
890
+
891
+ def search_by_file(self, file_pattern: str, limit: int = 50) -> str:
892
+ """
893
+ Search for entities by file path pattern.
894
+
895
+ Useful for finding all code elements in specific files or directories.
896
+
897
+ Args:
898
+ file_pattern: Glob-like pattern (e.g., "**/chat*.py", "api/v2/*.py", "rpc/*.py")
899
+ limit: Maximum entities to return
900
+ """
901
+ self._log_tool_event(f"Searching by file: {file_pattern}", "search_by_file")
902
+
903
+ entities = self._knowledge_graph.search_by_file(file_pattern, limit=limit)
904
+
905
+ if not entities:
906
+ return f"No entities found matching file pattern '{file_pattern}'"
907
+
908
+ output = f"# Entities from files matching '{file_pattern}' ({len(entities)})\n\n"
909
+
910
+ # Group by file
911
+ by_file: Dict[str, List] = {}
912
+ for ent in entities:
913
+ fp = ent.get('file_path', 'unknown')
914
+ if fp not in by_file:
915
+ by_file[fp] = []
916
+ by_file[fp].append(ent)
917
+
918
+ for fp, ents in sorted(by_file.items()):
919
+ output += f"## `{fp}` ({len(ents)} entities)\n"
920
+ for ent in ents[:15]:
921
+ output += f"- **{ent.get('name')}** ({ent.get('type', 'unknown')})\n"
922
+ if len(ents) > 15:
923
+ output += f"- ... and {len(ents) - 15} more\n"
924
+ output += "\n"
925
+
926
+ return output
927
+
928
+ def search_facts(
929
+ self,
930
+ query: Optional[str] = None,
931
+ fact_type: Optional[str] = None,
932
+ file_pattern: Optional[str] = None,
933
+ top_k: int = 20
934
+ ) -> str:
935
+ """
936
+ Search for semantic facts extracted from code and documentation.
937
+
938
+ Facts are structured knowledge extracted by LLM analysis:
939
+ - Code facts: algorithm, behavior, validation, dependency, error_handling
940
+ - Text facts: decision, requirement, definition, date, reference, contact
941
+
942
+ Each fact has subject-predicate-object structure with citations.
943
+
944
+ Args:
945
+ query: Optional text search on fact subject/content
946
+ fact_type: Filter by fact type (algorithm, behavior, decision, etc.)
947
+ file_pattern: Filter by source file path pattern
948
+ top_k: Maximum facts to return
949
+ """
950
+ self._log_tool_event(f"Searching facts: query={query}, type={fact_type}", "search_facts")
951
+
952
+ import re
953
+
954
+ results = []
955
+
956
+ # Compile file pattern regex if provided
957
+ file_regex = None
958
+ if file_pattern:
959
+ pattern = file_pattern.replace('.', r'\.').replace('**', '.*').replace('*', '[^/]*').replace('?', '.')
960
+ try:
961
+ file_regex = re.compile(pattern, re.IGNORECASE)
962
+ except re.error:
963
+ pass
964
+
965
+ # Search all fact entities
966
+ for node_id, data in self._knowledge_graph._graph.nodes(data=True):
967
+ # Only look at fact entities
968
+ if data.get('type', '').lower() != 'fact':
969
+ continue
970
+
971
+ # Filter by fact_type property
972
+ props = data.get('properties', {})
973
+ entity_fact_type = props.get('fact_type', '')
974
+
975
+ if fact_type and entity_fact_type.lower() != fact_type.lower():
976
+ continue
977
+
978
+ # Filter by file pattern
979
+ citations = data.get('citations', [])
980
+ if not citations and 'citation' in data:
981
+ citations = [data['citation']]
982
+
983
+ file_path = ''
984
+ for c in citations:
985
+ if isinstance(c, dict):
986
+ file_path = c.get('file_path', '')
987
+ break
988
+
989
+ if file_regex and file_path:
990
+ if not file_regex.search(file_path):
991
+ continue
992
+
993
+ # Filter by query (search in subject and predicate)
994
+ if query:
995
+ query_lower = query.lower()
996
+ subject = props.get('subject', '').lower()
997
+ predicate = props.get('predicate', '').lower()
998
+ obj = props.get('object', '').lower()
999
+ name = data.get('name', '').lower()
1000
+
1001
+ if not any(query_lower in text for text in [subject, predicate, obj, name]):
1002
+ continue
1003
+
1004
+ results.append({
1005
+ 'entity': dict(data),
1006
+ 'file_path': file_path,
1007
+ })
1008
+
1009
+ # Sort by file path, then by name
1010
+ results.sort(key=lambda x: (x['file_path'], x['entity'].get('name', '')))
1011
+ results = results[:top_k]
1012
+
1013
+ if not results:
1014
+ filters = []
1015
+ if query:
1016
+ filters.append(f"query='{query}'")
1017
+ if fact_type:
1018
+ filters.append(f"type={fact_type}")
1019
+ if file_pattern:
1020
+ filters.append(f"file={file_pattern}")
1021
+ filter_str = f" (filters: {', '.join(filters)})" if filters else ""
1022
+ return f"No facts found{filter_str}"
1023
+
1024
+ output = f"# Found {len(results)} facts\n\n"
1025
+
1026
+ # Group by fact type
1027
+ by_type: Dict[str, List] = {}
1028
+ for r in results:
1029
+ ft = r['entity'].get('properties', {}).get('fact_type', 'unknown')
1030
+ if ft not in by_type:
1031
+ by_type[ft] = []
1032
+ by_type[ft].append(r)
1033
+
1034
+ for ft, facts in sorted(by_type.items()):
1035
+ output += f"## {ft} ({len(facts)})\n\n"
1036
+ for f in facts:
1037
+ entity = f['entity']
1038
+ props = entity.get('properties', {})
1039
+ file_path = f['file_path']
1040
+
1041
+ subject = props.get('subject', entity.get('name', 'unknown'))
1042
+ predicate = props.get('predicate', '')
1043
+ obj = props.get('object', '')
1044
+ confidence = props.get('confidence', 0)
1045
+
1046
+ # Format as subject → predicate → object
1047
+ fact_text = f"**{subject}**"
1048
+ if predicate:
1049
+ fact_text += f" → {predicate}"
1050
+ if obj:
1051
+ fact_text += f" → {obj}"
1052
+
1053
+ output += f"- {fact_text}\n"
1054
+ if file_path:
1055
+ citation = entity.get('citation') or (entity.get('citations', [{}])[0] if entity.get('citations') else {})
1056
+ line_info = ""
1057
+ if isinstance(citation, dict) and citation.get('line_start'):
1058
+ line_info = f":{citation['line_start']}"
1059
+ if citation.get('line_end'):
1060
+ line_info += f"-{citation['line_end']}"
1061
+ output += f" 📍 `{file_path}{line_info}` (confidence: {confidence:.1%})\n"
1062
+ output += "\n"
1063
+
1064
+ return output
1065
+
1066
+ def get_file_info(self, file_path: str, include_entities: bool = True) -> str:
1067
+ """
1068
+ Get detailed information about a file node including all entities defined in it.
1069
+
1070
+ File nodes are container entities that aggregate all code, facts, and other
1071
+ entities from a single source file.
1072
+
1073
+ Args:
1074
+ file_path: Path to the file (can be partial, e.g., 'utils.py')
1075
+ include_entities: Whether to include list of entities defined in file
1076
+ """
1077
+ self._log_tool_event(f"Getting file info: {file_path}", "get_file_info")
1078
+
1079
+ # Search for file entities matching the path
1080
+ file_types = {'file', 'source_file', 'document_file', 'config_file', 'web_file'}
1081
+ matches = []
1082
+
1083
+ for node_id, data in self._knowledge_graph._graph.nodes(data=True):
1084
+ if data.get('type', '').lower() not in file_types:
1085
+ continue
1086
+
1087
+ # Match by full path or partial path
1088
+ entity_path = data.get('properties', {}).get('full_path', '') or data.get('name', '')
1089
+ if file_path in entity_path or entity_path.endswith(file_path):
1090
+ matches.append(data)
1091
+
1092
+ if not matches:
1093
+ return f"No file found matching '{file_path}'"
1094
+
1095
+ if len(matches) > 1:
1096
+ output = f"# Multiple files match '{file_path}'\n\n"
1097
+ for m in matches:
1098
+ full_path = m.get('properties', {}).get('full_path', m.get('name'))
1099
+ output += f"- `{full_path}`\n"
1100
+ output += f"\nShowing first match:\n\n"
1101
+ else:
1102
+ output = ""
1103
+
1104
+ file_entity = matches[0]
1105
+ props = file_entity.get('properties', {})
1106
+
1107
+ output += f"# {file_entity.get('name')}\n\n"
1108
+ output += f"**Type:** {file_entity.get('type')}\n"
1109
+ output += f"**Path:** `{props.get('full_path', file_entity.get('name'))}`\n"
1110
+ output += f"**Extension:** {props.get('extension', 'unknown')}\n"
1111
+ output += f"**Lines:** {props.get('line_count', 'unknown')}\n"
1112
+ output += f"**Size:** {props.get('size_bytes', 0):,} bytes\n"
1113
+ output += f"**Content Hash:** `{props.get('content_hash', 'unknown')[:12]}...`\n\n"
1114
+
1115
+ output += f"## Entity Summary\n\n"
1116
+ output += f"- **Code entities:** {props.get('code_entity_count', 0)}\n"
1117
+ output += f"- **Facts:** {props.get('fact_count', 0)}\n"
1118
+ output += f"- **Other entities:** {props.get('other_entity_count', 0)}\n"
1119
+ output += f"- **Total:** {props.get('entity_count', 0)}\n\n"
1120
+
1121
+ if include_entities:
1122
+ # Find entities defined_in this file
1123
+ file_id = file_entity.get('id')
1124
+ entities_in_file = []
1125
+
1126
+ for edge in self._knowledge_graph._graph.edges(data=True):
1127
+ source, target, edge_data = edge
1128
+ if target == file_id and edge_data.get('relation_type') == 'defined_in':
1129
+ entity = self._knowledge_graph.get_entity(source)
1130
+ if entity:
1131
+ entities_in_file.append(entity)
1132
+
1133
+ if entities_in_file:
1134
+ output += f"## Entities in File ({len(entities_in_file)})\n\n"
1135
+
1136
+ # Group by type
1137
+ by_type: Dict[str, List] = {}
1138
+ for ent in entities_in_file:
1139
+ etype = ent.get('type', 'unknown')
1140
+ if etype not in by_type:
1141
+ by_type[etype] = []
1142
+ by_type[etype].append(ent)
1143
+
1144
+ for etype, ents in sorted(by_type.items(), key=lambda x: -len(x[1])):
1145
+ output += f"### {etype} ({len(ents)})\n"
1146
+ for ent in ents[:10]:
1147
+ output += f"- **{ent.get('name')}**"
1148
+ if ent.get('type') == 'fact':
1149
+ fact_type = ent.get('properties', {}).get('fact_type', '')
1150
+ if fact_type:
1151
+ output += f" [{fact_type}]"
1152
+ output += "\n"
1153
+ if len(ents) > 10:
1154
+ output += f"- ... and {len(ents) - 10} more\n"
1155
+ output += "\n"
1156
+
1157
+ return output
1158
+
1159
+ def list_files(
1160
+ self,
1161
+ file_pattern: Optional[str] = None,
1162
+ file_type: Optional[str] = None,
1163
+ limit: int = 50
1164
+ ) -> str:
1165
+ """
1166
+ List all file nodes in the knowledge graph.
1167
+
1168
+ File nodes contain metadata about source files and link to all entities
1169
+ defined within them via 'defined_in' relationships.
1170
+
1171
+ Args:
1172
+ file_pattern: Optional glob pattern to filter files
1173
+ file_type: Filter by file type (source_file, document_file, config_file, web_file)
1174
+ limit: Maximum files to return
1175
+ """
1176
+ self._log_tool_event(f"Listing files: pattern={file_pattern}, type={file_type}", "list_files")
1177
+
1178
+ import re
1179
+
1180
+ file_types = {'file', 'source_file', 'document_file', 'config_file', 'web_file'}
1181
+
1182
+ # Compile pattern if provided
1183
+ file_regex = None
1184
+ if file_pattern:
1185
+ pattern = file_pattern.replace('.', r'\.').replace('**', '.*').replace('*', '[^/]*').replace('?', '.')
1186
+ try:
1187
+ file_regex = re.compile(pattern, re.IGNORECASE)
1188
+ except re.error:
1189
+ pass
1190
+
1191
+ files = []
1192
+ for node_id, data in self._knowledge_graph._graph.nodes(data=True):
1193
+ entity_type = data.get('type', '').lower()
1194
+ if entity_type not in file_types:
1195
+ continue
1196
+
1197
+ # Filter by file_type
1198
+ if file_type and entity_type != file_type.lower():
1199
+ continue
1200
+
1201
+ props = data.get('properties', {})
1202
+ full_path = props.get('full_path', data.get('name', ''))
1203
+
1204
+ # Filter by pattern
1205
+ if file_regex and not file_regex.search(full_path):
1206
+ continue
1207
+
1208
+ files.append({
1209
+ 'entity': data,
1210
+ 'path': full_path,
1211
+ })
1212
+
1213
+ if not files:
1214
+ filters = []
1215
+ if file_pattern:
1216
+ filters.append(f"pattern={file_pattern}")
1217
+ if file_type:
1218
+ filters.append(f"type={file_type}")
1219
+ filter_str = f" (filters: {', '.join(filters)})" if filters else ""
1220
+ return f"No files found{filter_str}"
1221
+
1222
+ # Sort by path
1223
+ files.sort(key=lambda x: x['path'])
1224
+ files = files[:limit]
1225
+
1226
+ output = f"# Files ({len(files)})\n\n"
1227
+
1228
+ # Group by file type
1229
+ by_type: Dict[str, List] = {}
1230
+ for f in files:
1231
+ ftype = f['entity'].get('type', 'file')
1232
+ if ftype not in by_type:
1233
+ by_type[ftype] = []
1234
+ by_type[ftype].append(f)
1235
+
1236
+ for ftype, flist in sorted(by_type.items()):
1237
+ output += f"## {ftype} ({len(flist)})\n\n"
1238
+ for f in flist:
1239
+ entity = f['entity']
1240
+ props = entity.get('properties', {})
1241
+ path = f['path']
1242
+ entity_count = props.get('entity_count', 0)
1243
+ fact_count = props.get('fact_count', 0)
1244
+
1245
+ output += f"- `{path}` ({entity_count} entities"
1246
+ if fact_count:
1247
+ output += f", {fact_count} facts"
1248
+ output += ")\n"
1249
+ output += "\n"
1250
+
1251
+ return output
1252
+
1253
+ def advanced_search(
1254
+ self,
1255
+ query: Optional[str] = None,
1256
+ entity_types: Optional[str] = None,
1257
+ layers: Optional[str] = None,
1258
+ file_patterns: Optional[str] = None,
1259
+ top_k: int = 20,
1260
+ ) -> str:
1261
+ """
1262
+ Advanced search with multiple filter criteria.
1263
+
1264
+ All filters use OR logic within each parameter.
1265
+
1266
+ Args:
1267
+ query: Text search query (optional)
1268
+ entity_types: Comma-separated types to include (e.g., "class,function,method")
1269
+ layers: Comma-separated layers to include (e.g., "code,service")
1270
+ file_patterns: Comma-separated file patterns (e.g., "api/*.py,rpc/*.py")
1271
+ top_k: Maximum results
1272
+ """
1273
+ self._log_tool_event(f"Advanced search: query={query}, types={entity_types}, layers={layers}, files={file_patterns}", "advanced_search")
1274
+
1275
+ # Parse comma-separated values
1276
+ types_list = [t.strip() for t in entity_types.split(',')] if entity_types else None
1277
+ layers_list = [l.strip() for l in layers.split(',')] if layers else None
1278
+ files_list = [f.strip() for f in file_patterns.split(',')] if file_patterns else None
1279
+
1280
+ results = self._knowledge_graph.search_advanced(
1281
+ query=query,
1282
+ entity_types=types_list,
1283
+ layers=layers_list,
1284
+ file_patterns=files_list,
1285
+ top_k=top_k,
1286
+ )
1287
+
1288
+ if not results:
1289
+ filters = []
1290
+ if entity_types:
1291
+ filters.append(f"types={entity_types}")
1292
+ if layers:
1293
+ filters.append(f"layers={layers}")
1294
+ if file_patterns:
1295
+ filters.append(f"files={file_patterns}")
1296
+ filter_str = f" (filters: {', '.join(filters)})" if filters else ""
1297
+ query_str = f" matching '{query}'" if query else ""
1298
+ return f"No entities found{query_str}{filter_str}"
1299
+
1300
+ output = f"# Advanced Search Results ({len(results)})\n\n"
1301
+
1302
+ for i, result in enumerate(results, 1):
1303
+ entity = result['entity']
1304
+ etype = entity.get('type', 'unknown')
1305
+ layer = entity.get('layer', '') or self._knowledge_graph.TYPE_TO_LAYER.get(etype.lower(), '')
1306
+ fp = entity.get('file_path', '')
1307
+
1308
+ type_str = f"{layer}/{etype}" if layer else etype
1309
+ output += f"{i:2}. **{entity.get('name')}** ({type_str})\n"
1310
+ if fp:
1311
+ output += f" 📍 `{fp}`\n"
1312
+ output += "\n"
1313
+
1314
+ return output
1315
+
1316
+ def get_available_tools(self) -> List[Dict[str, Any]]:
1317
+ """Return list of available retrieval tools."""
1318
+ return [
1319
+ {
1320
+ "name": "search_graph",
1321
+ "ref": self.search_graph,
1322
+ "description": "Search for entities with enhanced token matching. Supports 'chat message' finding 'ChatMessageHandler', file patterns like '**/chat*.py', and layer filtering (code, service, data, product, knowledge).",
1323
+ "args_schema": SearchGraphParams,
1324
+ },
1325
+ {
1326
+ "name": "search_facts",
1327
+ "ref": self.search_facts,
1328
+ "description": "Search semantic facts extracted from code and docs. Filter by fact_type: algorithm, behavior, validation (code) or decision, requirement, definition (text). Returns subject→predicate→object triples with citations.",
1329
+ "args_schema": SearchFactsParams,
1330
+ },
1331
+ {
1332
+ "name": "get_entity",
1333
+ "ref": self.get_entity,
1334
+ "description": "Get detailed information about a specific entity including properties and relations.",
1335
+ "args_schema": GetEntityParams,
1336
+ },
1337
+ {
1338
+ "name": "get_entity_content",
1339
+ "ref": self.get_entity_content,
1340
+ "description": "Retrieve source code for an entity using its citation. Reads from local files or remote toolkit.",
1341
+ "args_schema": GetEntityContentParams,
1342
+ },
1343
+ {
1344
+ "name": "search_by_file",
1345
+ "ref": self.search_by_file,
1346
+ "description": "Search entities by file path pattern. Use '**/chat*.py' to find all chat-related code, 'api/v2/*.py' for API v2 files.",
1347
+ "args_schema": SearchByFileParams,
1348
+ },
1349
+ {
1350
+ "name": "advanced_search",
1351
+ "ref": self.advanced_search,
1352
+ "description": "Advanced multi-criteria search. Combine text query with type/layer/file filters. Types: class,function,method. Layers: code,service,data,product,knowledge.",
1353
+ "args_schema": AdvancedSearchParams,
1354
+ },
1355
+ {
1356
+ "name": "impact_analysis",
1357
+ "ref": self.impact_analysis,
1358
+ "description": "Analyze what entities would be impacted by changes (downstream) or what this entity depends on (upstream).",
1359
+ "args_schema": ImpactAnalysisParams,
1360
+ },
1361
+ {
1362
+ "name": "get_related_entities",
1363
+ "ref": self.get_related_entities,
1364
+ "description": "Get entities related to a specific entity, optionally filtered by relation type.",
1365
+ "args_schema": GetRelatedEntitiesParams,
1366
+ },
1367
+ {
1368
+ "name": "get_stats",
1369
+ "ref": self.get_stats,
1370
+ "description": "Get statistics about the knowledge graph (entity counts, types, sources).",
1371
+ "args_schema": GetStatsParams,
1372
+ },
1373
+ {
1374
+ "name": "get_citations",
1375
+ "ref": self.get_citations,
1376
+ "description": "Get summary of entity citations by file. Shows which files contain which entities.",
1377
+ "args_schema": GetCitationsParams,
1378
+ },
1379
+ {
1380
+ "name": "list_entities_by_type",
1381
+ "ref": self.list_entities_by_type,
1382
+ "description": "List all entities of a specific type (class, function, api_endpoint, etc.). Case-insensitive.",
1383
+ "args_schema": ListEntitiesByTypeParams,
1384
+ },
1385
+ {
1386
+ "name": "list_entities_by_layer",
1387
+ "ref": self.list_entities_by_layer,
1388
+ "description": "List entities by semantic layer: code (classes/functions), service (APIs), data (models), product (features), knowledge (facts), structure (files), documentation, configuration, testing, tooling.",
1389
+ "args_schema": ListEntitiesByLayerParams,
1390
+ },
1391
+ {
1392
+ "name": "get_file_info",
1393
+ "ref": self.get_file_info,
1394
+ "description": "Get detailed info about a file including metadata (lines, size, hash) and all entities defined in it (classes, functions, facts, etc.).",
1395
+ "args_schema": GetFileInfoParams,
1396
+ },
1397
+ {
1398
+ "name": "list_files",
1399
+ "ref": self.list_files,
1400
+ "description": "List all file nodes in the graph. Filter by pattern ('**/*.py') or type (source_file, document_file, config_file). Shows entity counts per file.",
1401
+ "args_schema": ListFilesParams,
1402
+ },
1403
+ ]