elastro-client 1.3.32__tar.gz → 1.3.36__tar.gz

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 (131) hide show
  1. {elastro_client-1.3.32/elastro_client.egg-info → elastro_client-1.3.36}/PKG-INFO +6 -1
  2. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/__init__.py +1 -1
  3. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/cli.py +35 -1
  4. elastro_client-1.3.36/elastro/cli/commands/daemon.py +60 -0
  5. elastro_client-1.3.36/elastro/cli/commands/rag.py +72 -0
  6. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/client.py +1 -0
  7. elastro_client-1.3.36/elastro/core/daemon.py +132 -0
  8. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/document.py +2 -2
  9. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/document_bulk.py +2 -2
  10. elastro_client-1.3.36/elastro/core/rag/ast_parser.py +261 -0
  11. elastro_client-1.3.36/elastro/core/rag/ingestor.py +188 -0
  12. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/validation.py +1 -0
  13. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/server.py +5 -5
  14. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/utils/aliases.py +6 -6
  15. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/utils/health.py +2 -1
  16. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/utils/snapshots.py +1 -0
  17. {elastro_client-1.3.32 → elastro_client-1.3.36/elastro_client.egg-info}/PKG-INFO +6 -1
  18. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro_client.egg-info/SOURCES.txt +5 -0
  19. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro_client.egg-info/requires.txt +5 -0
  20. {elastro_client-1.3.32 → elastro_client-1.3.36}/pyproject.toml +17 -4
  21. {elastro_client-1.3.32 → elastro_client-1.3.36}/.coveragerc +0 -0
  22. {elastro_client-1.3.32 → elastro_client-1.3.36}/LICENSE +0 -0
  23. {elastro_client-1.3.32 → elastro_client-1.3.36}/MANIFEST.in +0 -0
  24. {elastro_client-1.3.32 → elastro_client-1.3.36}/README.md +0 -0
  25. {elastro_client-1.3.32 → elastro_client-1.3.36}/docs/advanced_features.md +0 -0
  26. {elastro_client-1.3.32 → elastro_client-1.3.36}/docs/api_reference.md +0 -0
  27. {elastro_client-1.3.32 → elastro_client-1.3.36}/docs/cli_usage.md +0 -0
  28. {elastro_client-1.3.32 → elastro_client-1.3.36}/docs/commands_reference.md +0 -0
  29. {elastro_client-1.3.32 → elastro_client-1.3.36}/docs/getting_started.md +0 -0
  30. {elastro_client-1.3.32 → elastro_client-1.3.36}/docs/roadmap.md +0 -0
  31. {elastro_client-1.3.32 → elastro_client-1.3.36}/docs/troubleshooting.md +0 -0
  32. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/advanced/__init__.py +0 -0
  33. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/advanced/aggregations.py +0 -0
  34. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/advanced/query_builder.py +0 -0
  35. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/advanced/scroll.py +0 -0
  36. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/__init__.py +0 -0
  37. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/art.py +0 -0
  38. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/__init__.py +0 -0
  39. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/cluster.py +0 -0
  40. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/config.py +0 -0
  41. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/datastream.py +0 -0
  42. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/document.py +0 -0
  43. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/gui.py +0 -0
  44. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/ilm.py +0 -0
  45. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/index.py +0 -0
  46. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/index_recipes.py +0 -0
  47. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/ingest.py +0 -0
  48. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/ml.py +0 -0
  49. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/painless_commands.py +0 -0
  50. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/script.py +0 -0
  51. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/security.py +0 -0
  52. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/snapshot.py +0 -0
  53. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/tasks.py +0 -0
  54. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/template.py +0 -0
  55. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/utils.py +0 -0
  56. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/completion.py +0 -0
  57. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/output.py +0 -0
  58. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/config/__init__.py +0 -0
  59. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/config/defaults.py +0 -0
  60. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/config/loader.py +0 -0
  61. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/__init__.py +0 -0
  62. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/datastream.py +0 -0
  63. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/errors.py +0 -0
  64. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/ilm.py +0 -0
  65. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/index.py +0 -0
  66. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/logger.py +0 -0
  67. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/query_builder.py +0 -0
  68. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/snapshot.py +0 -0
  69. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/gui/assets/index-CeUjjtn-.css +0 -0
  70. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/gui/assets/index-DNdGuJvV.js +0 -0
  71. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/gui/elastro.svg +0 -0
  72. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/gui/favicon.ico +0 -0
  73. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/gui/index.html +0 -0
  74. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/gui/vite.svg +0 -0
  75. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/py.typed +0 -0
  76. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/utils/__init__.py +0 -0
  77. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/utils/templates.py +0 -0
  78. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro_client.egg-info/dependency_links.txt +0 -0
  79. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro_client.egg-info/entry_points.txt +0 -0
  80. {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro_client.egg-info/top_level.txt +0 -0
  81. {elastro_client-1.3.32 → elastro_client-1.3.36}/examples/client.py +0 -0
  82. {elastro_client-1.3.32 → elastro_client-1.3.36}/examples/config_usage.py +0 -0
  83. {elastro_client-1.3.32 → elastro_client-1.3.36}/examples/datastreams.py +0 -0
  84. {elastro_client-1.3.32 → elastro_client-1.3.36}/examples/debug_connection.py +0 -0
  85. {elastro_client-1.3.32 → elastro_client-1.3.36}/examples/document_operations.py +0 -0
  86. {elastro_client-1.3.32 → elastro_client-1.3.36}/examples/index_management.py +0 -0
  87. {elastro_client-1.3.32 → elastro_client-1.3.36}/examples/search.py +0 -0
  88. {elastro_client-1.3.32 → elastro_client-1.3.36}/pytest.ini +0 -0
  89. {elastro_client-1.3.32 → elastro_client-1.3.36}/requirements.txt +0 -0
  90. {elastro_client-1.3.32 → elastro_client-1.3.36}/setup.cfg +0 -0
  91. {elastro_client-1.3.32 → elastro_client-1.3.36}/setup.py +0 -0
  92. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/__init__.py +0 -0
  93. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/conftest.py +0 -0
  94. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/fixtures/__init__.py +0 -0
  95. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/fixtures/datastream_fixtures.py +0 -0
  96. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/fixtures/document_fixtures.py +0 -0
  97. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/fixtures/index_fixtures.py +0 -0
  98. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/__init__.py +0 -0
  99. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/test_aggregations_integration.py +0 -0
  100. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/test_client_integration.py +0 -0
  101. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/test_datastream_integration.py +0 -0
  102. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/test_docs_quickstart.py +0 -0
  103. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/test_document_integration.py +0 -0
  104. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/test_index_integration.py +0 -0
  105. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/test_query_builder_integration.py +0 -0
  106. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/test_scroll_integration.py +0 -0
  107. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/test_workflow_integration.py +0 -0
  108. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/manual/test_es.py +0 -0
  109. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/__init__.py +0 -0
  110. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/advanced/__init__.py +0 -0
  111. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/advanced/test_aggregations.py +0 -0
  112. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/advanced/test_query_builder.py +0 -0
  113. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/advanced/test_scroll.py +0 -0
  114. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/config/__init__.py +0 -0
  115. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/config/test_defaults.py +0 -0
  116. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/config/test_loader.py +0 -0
  117. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/core/__init__.py +0 -0
  118. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/core/test_client.py +0 -0
  119. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/core/test_datastream.py +0 -0
  120. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/core/test_document.py +0 -0
  121. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/core/test_document_bulk.py +0 -0
  122. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/core/test_errors.py +0 -0
  123. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/core/test_index.py +0 -0
  124. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/core/test_validation.py +0 -0
  125. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/utils/__init__.py +0 -0
  126. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/utils/test_aliases.py +0 -0
  127. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/utils/test_health.py +0 -0
  128. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/utils/test_snapshots.py +0 -0
  129. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/utils/test_templates.py +0 -0
  130. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/verify_api_actions.py +0 -0
  131. {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/verify_cli_e2e.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elastro-client
3
- Version: 1.3.32
3
+ Version: 1.3.36
4
4
  Summary: A comprehensive Python library for Elasticsearch management with both programmatic and CLI interfaces
5
5
  Author: Austin Jorgensen
6
6
  License-Expression: MIT
@@ -29,6 +29,11 @@ Requires-Dist: rich>=10.0.0
29
29
  Requires-Dist: rich-click>=1.7.0
30
30
  Requires-Dist: fastapi>=0.111.0
31
31
  Requires-Dist: uvicorn>=0.30.0
32
+ Requires-Dist: tree-sitter>=0.23.0
33
+ Requires-Dist: tree-sitter-python>=0.23.0
34
+ Requires-Dist: tree-sitter-go>=0.23.0
35
+ Requires-Dist: tree-sitter-javascript>=0.23.0
36
+ Requires-Dist: tree-sitter-typescript>=0.23.0
32
37
  Provides-Extra: test
33
38
  Requires-Dist: pytest>=7.0.0; extra == "test"
34
39
  Requires-Dist: pytest-cov>=3.0.0; extra == "test"
@@ -4,7 +4,7 @@ Elasticsearch Management Module.
4
4
  A module for managing Elasticsearch operations within a pipeline process.
5
5
  """
6
6
 
7
- __version__ = "1.3.32"
7
+ __version__ = "1.3.36"
8
8
 
9
9
  # Core component imports
10
10
  from elastro.core.client import ElasticsearchClient
@@ -4,10 +4,40 @@ Command-line interface main module.
4
4
  This module defines the main CLI structure using Click.
5
5
  """
6
6
 
7
+ import sys
8
+
9
+ # --- FAST PATH INTERCEPTOR ---
10
+ # Must run before any other heavy imports to avoid the ~214ms startup penalty
11
+ if len(sys.argv) >= 3 and sys.argv[1] == "doc" and sys.argv[2] == "search":
12
+ # Only use the fast path if '--help' is not requested
13
+ if "--help" not in sys.argv and "-h" not in sys.argv:
14
+ try:
15
+ import json
16
+ import urllib.request
17
+ from http.client import HTTPResponse
18
+ from typing import cast, Dict, Any
19
+
20
+ req = urllib.request.Request(
21
+ "http://127.0.0.1:9201/fast-path/doc/search",
22
+ data=json.dumps({"args": sys.argv[3:]}).encode("utf-8"),
23
+ headers={"Content-Type": "application/json"},
24
+ method="POST",
25
+ )
26
+ with urllib.request.urlopen(req, timeout=0.05) as _response:
27
+ response = cast(HTTPResponse, _response)
28
+ if getattr(response, "status", 0) == 200:
29
+ print(response.read().decode("utf-8"), end="")
30
+ sys.exit(0)
31
+ except Exception:
32
+ # If the daemon is offline or crashes, silently fall through
33
+ # to the normal heavy Click execution.
34
+ pass
35
+ # --- END FAST PATH INTERCEPTOR ---
36
+
7
37
  import os
8
- import rich_click as click
9
38
  import json
10
39
  import yaml
40
+ import rich_click as click
11
41
  from typing import Dict, Any, Optional, List, Union
12
42
  from rich.console import Console
13
43
  from rich.table import Table
@@ -244,6 +274,8 @@ utils.add_command(utils_templates)
244
274
  utils.add_command(aliases)
245
275
 
246
276
  from elastro.cli.commands.gui import gui
277
+ from elastro.cli.commands.rag import rag_group
278
+ from elastro.cli.commands.daemon import daemon_group
247
279
 
248
280
  # Register Top-Level Groups
249
281
  cli.add_command(template_group)
@@ -257,6 +289,8 @@ cli.add_command(ml_group)
257
289
  cli.add_command(script_group)
258
290
  cli.add_command(painless_group)
259
291
  cli.add_command(gui)
292
+ cli.add_command(rag_group)
293
+ cli.add_command(daemon_group)
260
294
 
261
295
 
262
296
  def main() -> None:
@@ -0,0 +1,60 @@
1
+ """
2
+ Daemon management commands.
3
+ """
4
+
5
+ # fmt: off
6
+
7
+ import sys
8
+ import urllib.request
9
+
10
+ import rich_click as click
11
+
12
+
13
+ @click.group("daemon")
14
+ def daemon_group() -> None:
15
+ """
16
+ Manage the Fast-Path Agentic Daemon.
17
+ """
18
+ pass
19
+
20
+
21
+ @daemon_group.command("start")
22
+ @click.option("--port", type=int, default=9201, help="Port to run the daemon on")
23
+ @click.option(
24
+ "--host", type=str, default="127.0.0.1", help="Host to bind the daemon to"
25
+ )
26
+ def start(port: int, host: str) -> None:
27
+ """
28
+ Start the Fast-Path CLI Daemon.
29
+
30
+ Keeps the Elasticsearch connection hot in memory so subsequent agents querying via
31
+ 'elastro doc search' will bypass the slow Python startup sequence.
32
+ """
33
+ try:
34
+ from elastro.core.daemon import start_daemon
35
+
36
+ click.echo(f"Starting Elastro Daemon on {host}:{port}...")
37
+ start_daemon(host=host, port=port)
38
+ except ImportError as e:
39
+ click.echo(f"Failed to load daemon dependencies: {e}", err=True)
40
+ sys.exit(1)
41
+
42
+
43
+ @daemon_group.command("status")
44
+ @click.option("--port", type=int, default=9201, help="Port the daemon is running on")
45
+ def status(port: int) -> None:
46
+ """Check if the Daemon is active."""
47
+ try:
48
+ import urllib.request
49
+ from http.client import HTTPResponse
50
+ from typing import cast
51
+
52
+ req = urllib.request.Request(f"http://127.0.0.1:{port}/health")
53
+ with urllib.request.urlopen(req, timeout=1.0) as _response:
54
+ response = cast(HTTPResponse, _response)
55
+ if getattr(response, "status", 0) == 200:
56
+ click.echo("Daemon is ONLINE and responding.")
57
+ else:
58
+ click.echo(f"Daemon returned status: {getattr(response, 'status', 'unknown')}")
59
+ except Exception as e:
60
+ click.echo(f"Daemon is OFFLINE. ({e})")
@@ -0,0 +1,72 @@
1
+ """
2
+ Graph RAG Commands for Code Flow Mapping.
3
+ """
4
+
5
+ import rich_click as click
6
+ import os
7
+ from typing import Optional
8
+ from elastro.core.client import ElasticsearchClient
9
+ from elastro.core.rag.ingestor import GraphRAGManager
10
+ from elastro.cli.output import format_output
11
+ from rich.console import Console
12
+
13
+ console = Console()
14
+
15
+
16
+ @click.group("rag")
17
+ def rag_group() -> None:
18
+ """
19
+ Manage Graph RAG (Retrieval Augmented Generation) capabilities.
20
+
21
+ Includes native Code Flow mapping, AST extraction for Python, Go, TypeScript, and Vue,
22
+ and direct injection into local Elasticsearch for Agentic Codebase Memory.
23
+ """
24
+ pass
25
+
26
+
27
+ @rag_group.command("ingest")
28
+ @click.argument(
29
+ "repo_path", type=click.Path(exists=True, file_okay=False, dir_okay=True)
30
+ )
31
+ @click.option(
32
+ "--index",
33
+ "-i",
34
+ type=str,
35
+ default="fremen_codebase_rag",
36
+ help="Target Elasticsearch index name (default: fremen_codebase_rag)",
37
+ )
38
+ @click.pass_obj
39
+ def ingest_repo(client: ElasticsearchClient, repo_path: str, index: str) -> None:
40
+ """
41
+ Ingest a repository with AST Code Flow Mapping.
42
+
43
+ This command scans your codebase, utilizes Tree-sitter polyglot AST parsers
44
+ to extract 'functions_defined' and 'functions_called', and uses the
45
+ Elasticsearch Bulk API to stripe the Graph RAG data perfectly into an index.
46
+
47
+ Example:
48
+ ```bash
49
+ elastro rag ingest /path/to/my/go-repo
50
+ ```
51
+ """
52
+ repo_name = os.path.basename(os.path.abspath(repo_path))
53
+ console.print(
54
+ f"[bold cyan]🔍 Initializing Graph RAG AST Parsing for '{repo_name}'[/bold cyan]"
55
+ )
56
+
57
+ manager = GraphRAGManager(client, index)
58
+
59
+ try:
60
+ # We handle index scaffolding internally in GraphRAGManager
61
+ success_count = manager.ingest_repository(repo_path)
62
+
63
+ console.print(
64
+ f"[bold green]✅ Success![/bold green] Ingested and mapped Code Flows for [bold]{success_count}[/bold] files."
65
+ )
66
+ console.print(
67
+ f"[dim]The AST Graph RAG context is now active in index '{index}'.[/dim]"
68
+ )
69
+
70
+ except Exception as e:
71
+ console.print(f"[bold red]❌ RAG Ingestion Failed:[/bold red] {str(e)}")
72
+ exit(1)
@@ -1,3 +1,4 @@
1
+ # mypy: ignore-errors
1
2
  """
2
3
  Elasticsearch client module.
3
4
 
@@ -0,0 +1,132 @@
1
+ """
2
+ Elastro Fast-Path Daemon.
3
+
4
+ Maintains a persistent Elasticsearch client for sub-millisecond agentic queries.
5
+ """
6
+
7
+ import argparse
8
+ from contextlib import asynccontextmanager
9
+ from typing import List, Optional, AsyncGenerator, Any, Dict
10
+
11
+ import uvicorn
12
+ from fastapi import FastAPI, Request
13
+ from fastapi.responses import PlainTextResponse
14
+ from pydantic import BaseModel
15
+
16
+ from elastro.cli.output import format_output
17
+ from elastro.config.loader import get_config
18
+ from elastro.core.client import ElasticsearchClient
19
+ from elastro.core.document import DocumentManager
20
+ from elastro.core.query_builder import QueryBuilder
21
+
22
+ # Global client
23
+ client: Optional[ElasticsearchClient] = None
24
+
25
+
26
+ @asynccontextmanager
27
+ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
28
+ global client
29
+ config = get_config()
30
+ client = ElasticsearchClient(
31
+ hosts=config["elasticsearch"]["hosts"],
32
+ auth=config["elasticsearch"].get("auth"),
33
+ timeout=config["elasticsearch"].get("timeout", 30),
34
+ retry_on_timeout=config["elasticsearch"].get("retry_on_timeout", True),
35
+ max_retries=config["elasticsearch"].get("max_retries", 3),
36
+ )
37
+ client.connect()
38
+ yield
39
+ if client:
40
+ client.disconnect()
41
+
42
+
43
+ app = FastAPI(lifespan=lifespan, title="Elastro Agentic Daemon")
44
+
45
+
46
+ class DocSearchRequest(BaseModel):
47
+ args: List[str]
48
+
49
+
50
+ @app.get("/health")
51
+ def health_check() -> Dict[str, Any]:
52
+ return {
53
+ "status": "alive",
54
+ "client_connected": client.is_connected() if client else False,
55
+ }
56
+
57
+
58
+ @app.post("/fast-path/doc/search")
59
+ async def fast_path_search(req: DocSearchRequest) -> PlainTextResponse:
60
+ # Parse args manually for top 10 query types
61
+ parser = argparse.ArgumentParser()
62
+ parser.add_argument("index", type=str)
63
+ parser.add_argument("query", type=str, nargs="?", default=None)
64
+ parser.add_argument("--size", type=int, default=10)
65
+ parser.add_argument("--from", "from_", type=int, default=0, dest="from_")
66
+
67
+ # Query types
68
+ parser.add_argument("--match", action="append", default=[])
69
+ parser.add_argument(
70
+ "--match-phrase", action="append", default=[], dest="match_phrase"
71
+ )
72
+ parser.add_argument("--term", action="append", default=[])
73
+ parser.add_argument("--terms", action="append", default=[])
74
+ parser.add_argument("--range", action="append", default=[])
75
+ parser.add_argument("--prefix", action="append", default=[])
76
+ parser.add_argument("--wildcard", action="append", default=[])
77
+ parser.add_argument("--exists", action="append", default=[])
78
+ parser.add_argument("--ids", action="append", default=[])
79
+ parser.add_argument("--fuzzy", action="append", default=[])
80
+
81
+ # Excludes
82
+ parser.add_argument(
83
+ "--exclude-match", action="append", default=[], dest="exclude_match"
84
+ )
85
+ parser.add_argument(
86
+ "--exclude-term", action="append", default=[], dest="exclude_term"
87
+ )
88
+
89
+ parser.add_argument("--output", "-o", type=str, default="json")
90
+
91
+ parsed, _ = parser.parse_known_args(req.args)
92
+
93
+ if not client:
94
+ return PlainTextResponse(
95
+ "Error: Elasticsearch client not connected in daemon.", status_code=500
96
+ )
97
+
98
+ doc_manager = DocumentManager(client)
99
+
100
+ inner_query = QueryBuilder.build_bool_query(
101
+ must_match=getattr(parsed, "match", None),
102
+ must_match_phrase=getattr(parsed, "match_phrase", None),
103
+ must_term=getattr(parsed, "term", None),
104
+ must_terms=getattr(parsed, "terms", None),
105
+ must_range=getattr(parsed, "range", None),
106
+ must_prefix=getattr(parsed, "prefix", None),
107
+ must_wildcard=getattr(parsed, "wildcard", None),
108
+ must_exists=getattr(parsed, "exists", None),
109
+ must_ids=getattr(parsed, "ids", None),
110
+ must_fuzzy=getattr(parsed, "fuzzy", None),
111
+ exclude_match=getattr(parsed, "exclude_match", None),
112
+ exclude_term=getattr(parsed, "exclude_term", None),
113
+ query_string=getattr(parsed, "query", None),
114
+ )
115
+
116
+ query_body = {"query": inner_query}
117
+ options = {"size": getattr(parsed, "size", 10), "from": getattr(parsed, "from_", 0)}
118
+
119
+ try:
120
+ results = doc_manager.search(getattr(parsed, "index", ""), query_body, options)
121
+ output_format = getattr(parsed, "output", "json")
122
+ output_str = format_output(results, output_format=output_format)
123
+ return PlainTextResponse(content=output_str)
124
+ except Exception as e:
125
+ return PlainTextResponse(
126
+ content=f"Daemon search error: {str(e)}", status_code=500
127
+ )
128
+
129
+
130
+ def start_daemon(host: str = "127.0.0.1", port: int = 9201) -> None:
131
+ """Start the Elastro daemon server."""
132
+ uvicorn.run(app, host=host, port=port, log_level="info")
@@ -74,7 +74,7 @@ class DocumentManager:
74
74
 
75
75
  logger.debug(f"Indexing document into '{index}' with ID '{id}'")
76
76
  # Execute the index operation
77
- response = self.client.client.index(**params)
77
+ response = self.client.client.index(**params) # type: ignore
78
78
  return response.body if hasattr(response, "body") else dict(response)
79
79
  except Exception as e:
80
80
  logger.error(f"Failed to index document info '{index}': {str(e)}")
@@ -355,7 +355,7 @@ class DocumentManager:
355
355
 
356
356
  try:
357
357
  logger.debug(f"Searching index '{index}'...")
358
- response = self.client.client.search(**search_params)
358
+ response = self.client.client.search(**search_params) # type: ignore
359
359
  return response.body if hasattr(response, "body") else dict(response)
360
360
  except Exception as e:
361
361
  logger.error(f"Failed to search documents in '{index}': {str(e)}")
@@ -74,7 +74,7 @@ class BulkDocumentManager:
74
74
  # Execute bulk operation
75
75
  return self.client.client.bulk(
76
76
  operations=operations, refresh="true" if refresh else "false"
77
- )
77
+ ) # type: ignore
78
78
  except Exception as e:
79
79
  raise DocumentError(f"Failed to bulk index documents: {str(e)}")
80
80
 
@@ -112,6 +112,6 @@ class BulkDocumentManager:
112
112
  # Execute bulk operation
113
113
  return self.client.client.bulk(
114
114
  operations=operations, refresh="true" if refresh else "false"
115
- )
115
+ ) # type: ignore
116
116
  except Exception as e:
117
117
  raise DocumentError(f"Failed to bulk delete documents: {str(e)}")
@@ -0,0 +1,261 @@
1
+ # mypy: ignore-errors
2
+ """
3
+ Abstract Syntax Tree (AST) parser utilizing tree-sitter for Graph RAG Code Flow Mapping.
4
+ Supports polyglot analysis of Python, Go, JavaScript, TypeScript, and Vue.
5
+ Implements Semantic Chunking and Signature Extraction.
6
+ """
7
+
8
+ import os
9
+ import tree_sitter # type: ignore
10
+ from typing import Dict, List, Set, Any
11
+
12
+
13
+ class ASTParser:
14
+ def __init__(self) -> None:
15
+ self.parsers: Dict[str, tree_sitter.Parser] = {}
16
+ self._initialize_parsers()
17
+
18
+ def _initialize_parsers(self) -> None:
19
+ """Loads natively compiled tree-sitter language capsules."""
20
+ try:
21
+ from tree_sitter import Language, Parser
22
+ import tree_sitter_python as tsp # type: ignore
23
+ import tree_sitter_go as tsg # type: ignore
24
+ import tree_sitter_javascript as tsjs # type: ignore
25
+ import tree_sitter_typescript as tsts # type: ignore
26
+
27
+ # v0.25+ API natively imports .language() as PyCapsule pointers
28
+ self.langs = {
29
+ "python": Language(tsp.language()),
30
+ "go": Language(tsg.language()),
31
+ "javascript": Language(tsjs.language()),
32
+ "typescript": Language(tsts.language_typescript()),
33
+ # Vue uses specialized HTML embedding, but for script logic, TS/JS parser handles it well
34
+ "vue": Language(tsts.language_typescript()),
35
+ }
36
+
37
+ for lang_name, lang_obj in self.langs.items():
38
+ parser = Parser()
39
+ parser.language = lang_obj
40
+ self.parsers[lang_name] = parser
41
+
42
+ except ImportError as e:
43
+ # Degrade gracefully if tree-sitter is missing during testing/CI
44
+ print(f"Graph RAG AST Parser initialization warning: {e}")
45
+ pass
46
+
47
+ def _determine_language(self, ext: str) -> str:
48
+ ext = ext.lower()
49
+ if ext == ".py":
50
+ return "python"
51
+ elif ext == ".go":
52
+ return "go"
53
+ elif ext in [".js", ".jsx"]:
54
+ return "javascript"
55
+ elif ext in [".ts", ".tsx"]:
56
+ return "typescript"
57
+ elif ext == ".vue":
58
+ return "vue"
59
+ return "unsupported"
60
+
61
+ def parse_file(self, file_path: str, content: str) -> List[Dict[str, Any]]:
62
+ """
63
+ Parses raw code and extracts semantic chunks (functions/classes) with deterministic call graphs.
64
+ """
65
+ ext = os.path.splitext(file_path)[1]
66
+ lang = self._determine_language(ext)
67
+
68
+ if lang not in self.parsers:
69
+ return [
70
+ {
71
+ "chunk_type": "file",
72
+ "name": "module",
73
+ "content": content,
74
+ "functions_defined": [],
75
+ "functions_called": [],
76
+ }
77
+ ]
78
+
79
+ parser = self.parsers[lang]
80
+ source = content.encode("utf-8")
81
+ tree = parser.parse(source)
82
+
83
+ chunks: List[Dict[str, Any]] = []
84
+
85
+ self._extract_chunks(tree.root_node, lang, source, chunks)
86
+
87
+ # If no semantic chunks were found, fallback to parsing the whole file as a single module chunk
88
+ if not chunks:
89
+ called: Set[str] = set()
90
+ self._traverse_for_calls(tree.root_node, lang, source, called)
91
+ chunks.append(
92
+ {
93
+ "chunk_type": "file",
94
+ "name": "module",
95
+ "content": content,
96
+ "functions_defined": [],
97
+ "functions_called": sorted(list(called)),
98
+ }
99
+ )
100
+
101
+ return chunks
102
+
103
+ def _extract_chunks(
104
+ self,
105
+ node: tree_sitter.Node,
106
+ lang: str,
107
+ source: bytes,
108
+ chunks: List[Dict[str, Any]],
109
+ ) -> None:
110
+ """Recursive AST Walk for semantic chunking."""
111
+ node_type = node.type
112
+
113
+ is_chunk = False
114
+ chunk_name = ""
115
+ chunk_type = ""
116
+
117
+ if lang == "python":
118
+ if node_type in ["function_definition", "class_definition"]:
119
+ is_chunk = True
120
+ chunk_type = "function" if "function" in node_type else "class"
121
+ name_node = node.child_by_field_name("name")
122
+ if name_node:
123
+ chunk_name = source[
124
+ name_node.start_byte : name_node.end_byte
125
+ ].decode("utf-8")
126
+
127
+ elif lang == "go":
128
+ if node_type in [
129
+ "function_declaration",
130
+ "method_declaration",
131
+ "type_declaration",
132
+ ]:
133
+ is_chunk = True
134
+ chunk_type = (
135
+ "function"
136
+ if "function" in node_type or "method" in node_type
137
+ else "class"
138
+ )
139
+ name_node = node.child_by_field_name("name")
140
+ if name_node:
141
+ chunk_name = source[
142
+ name_node.start_byte : name_node.end_byte
143
+ ].decode("utf-8")
144
+
145
+ elif lang in ["javascript", "typescript", "vue"]:
146
+ if node_type in [
147
+ "function_declaration",
148
+ "method_definition",
149
+ "class_declaration",
150
+ ]:
151
+ is_chunk = True
152
+ chunk_type = (
153
+ "function"
154
+ if "function" in node_type or "method" in node_type
155
+ else "class"
156
+ )
157
+ name_node = node.child_by_field_name("name")
158
+ if name_node:
159
+ chunk_name = source[
160
+ name_node.start_byte : name_node.end_byte
161
+ ].decode("utf-8")
162
+ elif node_type == "variable_declarator":
163
+ name_node = node.child_by_field_name("name")
164
+ val_node = node.child_by_field_name("value")
165
+ if name_node and val_node and val_node.type == "arrow_function":
166
+ is_chunk = True
167
+ chunk_type = "function"
168
+ chunk_name = source[
169
+ name_node.start_byte : name_node.end_byte
170
+ ].decode("utf-8")
171
+
172
+ if is_chunk:
173
+ called: Set[str] = set()
174
+ # Traverse specifically inside this chunk's boundaries to extract calls
175
+ self._traverse_for_calls(node, lang, source, called)
176
+
177
+ # To ensure proper context window, extract the source of just this node
178
+ chunk_content = source[node.start_byte : node.end_byte].decode("utf-8")
179
+
180
+ chunks.append(
181
+ {
182
+ "chunk_type": chunk_type,
183
+ "name": chunk_name or "anonymous",
184
+ "content": chunk_content,
185
+ "functions_defined": [chunk_name] if chunk_name else [],
186
+ "functions_called": sorted(list(called)),
187
+ }
188
+ )
189
+
190
+ # Continue recursing to find nested functions/classes (e.g., methods inside a class)
191
+ for child in node.children:
192
+ self._extract_chunks(child, lang, source, chunks)
193
+
194
+ def _traverse_for_calls(
195
+ self, node: tree_sitter.Node, lang: str, source: bytes, called: Set[str]
196
+ ) -> None:
197
+ """Extracts call chains including their parameters/arguments for exact signature mapping."""
198
+ if node.type == "call_expression":
199
+ func_node = node.child_by_field_name("function")
200
+ if func_node:
201
+ call_text = self._extract_call_chain(func_node, source)
202
+
203
+ ignore_list = {
204
+ "print",
205
+ "len",
206
+ "range",
207
+ "str",
208
+ "int",
209
+ "list",
210
+ "dict",
211
+ "set",
212
+ "super",
213
+ "append",
214
+ "fmt.Println",
215
+ "console.log",
216
+ "console.error",
217
+ "require",
218
+ "getattr",
219
+ "setattr",
220
+ "hasattr",
221
+ "isinstance",
222
+ "type",
223
+ }
224
+
225
+ if call_text and call_text not in ignore_list:
226
+ # Signature Extraction: Pull the arguments as well
227
+ args_node = node.child_by_field_name("arguments")
228
+ if args_node:
229
+ args_text = source[
230
+ args_node.start_byte : args_node.end_byte
231
+ ].decode("utf-8")
232
+ # Sanitize whitespace/newlines
233
+ args_text = " ".join(args_text.split())
234
+ if len(args_text) < 60:
235
+ call_text = f"{call_text}{args_text}"
236
+
237
+ if len(call_text) < 120 and "\n" not in call_text:
238
+ called.add(call_text)
239
+
240
+ for child in node.children:
241
+ self._traverse_for_calls(child, lang, source, called)
242
+
243
+ def _extract_call_chain(self, node: tree_sitter.Node, source: bytes) -> str:
244
+ """Recursively builds pure string representation of chained object method calls."""
245
+ if node.type == "identifier":
246
+ return source[node.start_byte : node.end_byte].decode("utf-8")
247
+ elif node.type == "attribute":
248
+ obj_node = node.child_by_field_name("object")
249
+ attr_node = node.child_by_field_name("attribute")
250
+ if obj_node and attr_node:
251
+ obj_str = self._extract_call_chain(obj_node, source)
252
+ attr_str = source[attr_node.start_byte : attr_node.end_byte].decode(
253
+ "utf-8"
254
+ )
255
+ return f"{obj_str}.{attr_str}"
256
+ elif node.type == "call":
257
+ func_node = node.child_by_field_name("function")
258
+ if func_node:
259
+ return self._extract_call_chain(func_node, source)
260
+
261
+ return source[node.start_byte : node.end_byte].decode("utf-8").strip()