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.
- {elastro_client-1.3.32/elastro_client.egg-info → elastro_client-1.3.36}/PKG-INFO +6 -1
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/__init__.py +1 -1
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/cli.py +35 -1
- elastro_client-1.3.36/elastro/cli/commands/daemon.py +60 -0
- elastro_client-1.3.36/elastro/cli/commands/rag.py +72 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/client.py +1 -0
- elastro_client-1.3.36/elastro/core/daemon.py +132 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/document.py +2 -2
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/document_bulk.py +2 -2
- elastro_client-1.3.36/elastro/core/rag/ast_parser.py +261 -0
- elastro_client-1.3.36/elastro/core/rag/ingestor.py +188 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/validation.py +1 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/server.py +5 -5
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/utils/aliases.py +6 -6
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/utils/health.py +2 -1
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/utils/snapshots.py +1 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36/elastro_client.egg-info}/PKG-INFO +6 -1
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro_client.egg-info/SOURCES.txt +5 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro_client.egg-info/requires.txt +5 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/pyproject.toml +17 -4
- {elastro_client-1.3.32 → elastro_client-1.3.36}/.coveragerc +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/LICENSE +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/MANIFEST.in +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/README.md +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/docs/advanced_features.md +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/docs/api_reference.md +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/docs/cli_usage.md +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/docs/commands_reference.md +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/docs/getting_started.md +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/docs/roadmap.md +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/docs/troubleshooting.md +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/advanced/__init__.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/advanced/aggregations.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/advanced/query_builder.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/advanced/scroll.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/__init__.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/art.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/__init__.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/cluster.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/config.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/datastream.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/document.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/gui.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/ilm.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/index.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/index_recipes.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/ingest.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/ml.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/painless_commands.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/script.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/security.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/snapshot.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/tasks.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/template.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/commands/utils.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/completion.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/cli/output.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/config/__init__.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/config/defaults.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/config/loader.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/__init__.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/datastream.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/errors.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/ilm.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/index.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/logger.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/query_builder.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/core/snapshot.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/gui/assets/index-CeUjjtn-.css +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/gui/assets/index-DNdGuJvV.js +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/gui/elastro.svg +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/gui/favicon.ico +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/gui/index.html +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/gui/vite.svg +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/py.typed +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/utils/__init__.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro/utils/templates.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro_client.egg-info/dependency_links.txt +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro_client.egg-info/entry_points.txt +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/elastro_client.egg-info/top_level.txt +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/examples/client.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/examples/config_usage.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/examples/datastreams.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/examples/debug_connection.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/examples/document_operations.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/examples/index_management.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/examples/search.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/pytest.ini +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/requirements.txt +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/setup.cfg +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/setup.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/__init__.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/conftest.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/fixtures/__init__.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/fixtures/datastream_fixtures.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/fixtures/document_fixtures.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/fixtures/index_fixtures.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/__init__.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/test_aggregations_integration.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/test_client_integration.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/test_datastream_integration.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/test_docs_quickstart.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/test_document_integration.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/test_index_integration.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/test_query_builder_integration.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/test_scroll_integration.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/integration/test_workflow_integration.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/manual/test_es.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/__init__.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/advanced/__init__.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/advanced/test_aggregations.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/advanced/test_query_builder.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/advanced/test_scroll.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/config/__init__.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/config/test_defaults.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/config/test_loader.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/core/__init__.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/core/test_client.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/core/test_datastream.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/core/test_document.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/core/test_document_bulk.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/core/test_errors.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/core/test_index.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/core/test_validation.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/utils/__init__.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/utils/test_aliases.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/utils/test_health.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/utils/test_snapshots.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/unit/utils/test_templates.py +0 -0
- {elastro_client-1.3.32 → elastro_client-1.3.36}/tests/verify_api_actions.py +0 -0
- {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.
|
|
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,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)
|
|
@@ -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()
|