gigaflow 0.1.0__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.
- gigaflow-0.1.0/PKG-INFO +124 -0
- gigaflow-0.1.0/README.md +100 -0
- gigaflow-0.1.0/gigaflow/__init__.py +1 -0
- gigaflow-0.1.0/gigaflow/_config.py +24 -0
- gigaflow-0.1.0/gigaflow/_fmt.py +78 -0
- gigaflow-0.1.0/gigaflow/_http.py +24 -0
- gigaflow-0.1.0/gigaflow/_setup.py +230 -0
- gigaflow-0.1.0/gigaflow/cli.py +90 -0
- gigaflow-0.1.0/gigaflow/commands/__init__.py +1 -0
- gigaflow-0.1.0/gigaflow/commands/config.py +37 -0
- gigaflow-0.1.0/gigaflow/commands/inspect.py +1260 -0
- gigaflow-0.1.0/gigaflow/commands/projects.py +48 -0
- gigaflow-0.1.0/gigaflow/commands/run.py +113 -0
- gigaflow-0.1.0/gigaflow/commands/setup.py +43 -0
- gigaflow-0.1.0/gigaflow/commands/traces.py +100 -0
- gigaflow-0.1.0/gigaflow/transforms/__init__.py +0 -0
- gigaflow-0.1.0/gigaflow/transforms/arize_phoenix.yml +42 -0
- gigaflow-0.1.0/gigaflow.egg-info/PKG-INFO +124 -0
- gigaflow-0.1.0/gigaflow.egg-info/SOURCES.txt +26 -0
- gigaflow-0.1.0/gigaflow.egg-info/dependency_links.txt +1 -0
- gigaflow-0.1.0/gigaflow.egg-info/entry_points.txt +2 -0
- gigaflow-0.1.0/gigaflow.egg-info/requires.txt +5 -0
- gigaflow-0.1.0/gigaflow.egg-info/top_level.txt +1 -0
- gigaflow-0.1.0/pyproject.toml +69 -0
- gigaflow-0.1.0/setup.cfg +4 -0
- gigaflow-0.1.0/tests/test_commands.py +621 -0
- gigaflow-0.1.0/tests/test_default_transform.py +62 -0
- gigaflow-0.1.0/tests/test_load_env_file.py +92 -0
gigaflow-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gigaflow
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: GigaFlow CLI — connect Arize Phoenix traces to GigaFlow and analyze them
|
|
5
|
+
Author: GigaFlow Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/GigaFlow-AI/gigaflow
|
|
8
|
+
Project-URL: Repository, https://github.com/GigaFlow-AI/gigaflow
|
|
9
|
+
Keywords: gigaflow,arize,phoenix,llm,observability,tracing,cli
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Environment :: Console
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
22
|
+
Requires-Dist: ruff>=0.1.6; extra == "dev"
|
|
23
|
+
Requires-Dist: mypy>=1.7.0; extra == "dev"
|
|
24
|
+
|
|
25
|
+
# gigaflow CLI
|
|
26
|
+
|
|
27
|
+
Command-line interface for GigaFlow — connect Arize Phoenix traces to GigaFlow and analyze them with Atomic Information Flow (AIF).
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install gigaflow
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or from source:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
cd cli && pip install -e .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
gigaflow setup # Connect to Arize Phoenix datasource
|
|
45
|
+
gigaflow traces # List traces (auto-syncs first)
|
|
46
|
+
gigaflow spans <trace_id> # List spans for a trace
|
|
47
|
+
gigaflow aif run <trace_id> # Run AIF analysis
|
|
48
|
+
gigaflow aif run <trace_id> --model gpt-4o --show-atoms
|
|
49
|
+
gigaflow sync # Re-sync from datasource
|
|
50
|
+
gigaflow config show # Show saved config
|
|
51
|
+
gigaflow config clear # Reset config
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Transform config
|
|
55
|
+
|
|
56
|
+
GigaFlow maps raw Arize Phoenix spans to its own primitives (`llm_call`, `tool_invocation`, `user_input`) using a YAML transform config.
|
|
57
|
+
|
|
58
|
+
The built-in config for Arize Phoenix is at:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
gigaflow/transforms/arize_phoenix.yml
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
It maps the OpenInference span columns (`span_kind`, `attributes.*`) to the fields that AIF reads. If your Arize setup uses different attribute paths, copy the file, edit it, and provide the path during `gigaflow setup` when prompted:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
Path to transform.yml (leave blank for built-in Arize Phoenix config): /path/to/my_transform.yml
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
You can also re-upload a transform config to an existing project without re-running the full wizard:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
curl -X PUT http://localhost:8000/api/v1/projects/<project_id>/transform \
|
|
74
|
+
-H "Content-Type: text/plain" \
|
|
75
|
+
--data-binary @my_transform.yml
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
After changing the transform config, re-sync to reclassify your spans:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
gigaflow sync
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Transform config format
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
version: "1.0"
|
|
88
|
+
source: arize_phoenix
|
|
89
|
+
|
|
90
|
+
primitives:
|
|
91
|
+
llm_call:
|
|
92
|
+
filter:
|
|
93
|
+
field: span_kind # top-level DB column
|
|
94
|
+
value: "LLM"
|
|
95
|
+
mapping:
|
|
96
|
+
completion: attributes.output.value # read by AIF for response atoms
|
|
97
|
+
model: attributes.llm.model_name
|
|
98
|
+
|
|
99
|
+
tool_invocation:
|
|
100
|
+
filter:
|
|
101
|
+
field: span_kind
|
|
102
|
+
value: "TOOL"
|
|
103
|
+
mapping:
|
|
104
|
+
tool_output: attributes.output.value # read by AIF for tool atoms
|
|
105
|
+
tool_name: attributes.tool.name
|
|
106
|
+
|
|
107
|
+
user_input:
|
|
108
|
+
filter:
|
|
109
|
+
field: span_kind
|
|
110
|
+
value: "CHAIN"
|
|
111
|
+
mapping:
|
|
112
|
+
content: attributes.input.value # read by AIF for the user query
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Mapping values are dot-notation paths traversed against each span row. Nested JSON columns (e.g. `attributes`) are parsed automatically.
|
|
116
|
+
|
|
117
|
+
## Publish to PyPI
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
cd cli
|
|
121
|
+
pip install build twine
|
|
122
|
+
python -m build
|
|
123
|
+
twine upload dist/*
|
|
124
|
+
```
|
gigaflow-0.1.0/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# gigaflow CLI
|
|
2
|
+
|
|
3
|
+
Command-line interface for GigaFlow — connect Arize Phoenix traces to GigaFlow and analyze them with Atomic Information Flow (AIF).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install gigaflow
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or from source:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd cli && pip install -e .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
gigaflow setup # Connect to Arize Phoenix datasource
|
|
21
|
+
gigaflow traces # List traces (auto-syncs first)
|
|
22
|
+
gigaflow spans <trace_id> # List spans for a trace
|
|
23
|
+
gigaflow aif run <trace_id> # Run AIF analysis
|
|
24
|
+
gigaflow aif run <trace_id> --model gpt-4o --show-atoms
|
|
25
|
+
gigaflow sync # Re-sync from datasource
|
|
26
|
+
gigaflow config show # Show saved config
|
|
27
|
+
gigaflow config clear # Reset config
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Transform config
|
|
31
|
+
|
|
32
|
+
GigaFlow maps raw Arize Phoenix spans to its own primitives (`llm_call`, `tool_invocation`, `user_input`) using a YAML transform config.
|
|
33
|
+
|
|
34
|
+
The built-in config for Arize Phoenix is at:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
gigaflow/transforms/arize_phoenix.yml
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
It maps the OpenInference span columns (`span_kind`, `attributes.*`) to the fields that AIF reads. If your Arize setup uses different attribute paths, copy the file, edit it, and provide the path during `gigaflow setup` when prompted:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
Path to transform.yml (leave blank for built-in Arize Phoenix config): /path/to/my_transform.yml
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
You can also re-upload a transform config to an existing project without re-running the full wizard:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
curl -X PUT http://localhost:8000/api/v1/projects/<project_id>/transform \
|
|
50
|
+
-H "Content-Type: text/plain" \
|
|
51
|
+
--data-binary @my_transform.yml
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
After changing the transform config, re-sync to reclassify your spans:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
gigaflow sync
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Transform config format
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
version: "1.0"
|
|
64
|
+
source: arize_phoenix
|
|
65
|
+
|
|
66
|
+
primitives:
|
|
67
|
+
llm_call:
|
|
68
|
+
filter:
|
|
69
|
+
field: span_kind # top-level DB column
|
|
70
|
+
value: "LLM"
|
|
71
|
+
mapping:
|
|
72
|
+
completion: attributes.output.value # read by AIF for response atoms
|
|
73
|
+
model: attributes.llm.model_name
|
|
74
|
+
|
|
75
|
+
tool_invocation:
|
|
76
|
+
filter:
|
|
77
|
+
field: span_kind
|
|
78
|
+
value: "TOOL"
|
|
79
|
+
mapping:
|
|
80
|
+
tool_output: attributes.output.value # read by AIF for tool atoms
|
|
81
|
+
tool_name: attributes.tool.name
|
|
82
|
+
|
|
83
|
+
user_input:
|
|
84
|
+
filter:
|
|
85
|
+
field: span_kind
|
|
86
|
+
value: "CHAIN"
|
|
87
|
+
mapping:
|
|
88
|
+
content: attributes.input.value # read by AIF for the user query
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Mapping values are dot-notation paths traversed against each span row. Nested JSON columns (e.g. `attributes`) are parsed automatically.
|
|
92
|
+
|
|
93
|
+
## Publish to PyPI
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
cd cli
|
|
97
|
+
pip install build twine
|
|
98
|
+
python -m build
|
|
99
|
+
twine upload dist/*
|
|
100
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""GigaFlow CLI package."""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Persistent CLI configuration stored in ~/.gigaflow/config.json."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
CONFIG_PATH = Path.home() / ".gigaflow" / "config.json"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load() -> dict:
|
|
10
|
+
if not CONFIG_PATH.exists():
|
|
11
|
+
return {}
|
|
12
|
+
with open(CONFIG_PATH) as f:
|
|
13
|
+
return json.load(f)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def save(config: dict):
|
|
17
|
+
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
with open(CONFIG_PATH, "w") as f:
|
|
19
|
+
json.dump(config, f, indent=2)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def clear():
|
|
23
|
+
if CONFIG_PATH.exists():
|
|
24
|
+
CONFIG_PATH.unlink()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Terminal formatting: headers, prompts, tables."""
|
|
2
|
+
|
|
3
|
+
import getpass
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
WIDTH = 62
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def header(title: str):
|
|
10
|
+
print()
|
|
11
|
+
print("=" * WIDTH)
|
|
12
|
+
print(f" {title}")
|
|
13
|
+
print("=" * WIDTH)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def section(title: str):
|
|
17
|
+
pad = max(0, WIDTH - len(title) - 4)
|
|
18
|
+
print(f"\n── {title} {'─' * pad}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def ok(msg: str):
|
|
22
|
+
print(f" ✓ {msg}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def fail(msg: str):
|
|
26
|
+
print(f" ✗ {msg}", file=sys.stderr)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def info(msg: str):
|
|
30
|
+
print(f" {msg}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def prompt(label: str, default: str = "", required: bool = False) -> str:
|
|
34
|
+
suffix = f" [{default}]" if default else ""
|
|
35
|
+
while True:
|
|
36
|
+
try:
|
|
37
|
+
val = input(f" {label}{suffix}: ").strip()
|
|
38
|
+
except (EOFError, KeyboardInterrupt):
|
|
39
|
+
print()
|
|
40
|
+
sys.exit(0)
|
|
41
|
+
result = val if val else default
|
|
42
|
+
if result or not required:
|
|
43
|
+
return result
|
|
44
|
+
print(f" {label} is required.")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def prompt_password(label: str) -> str:
|
|
48
|
+
try:
|
|
49
|
+
return getpass.getpass(f" {label}: ")
|
|
50
|
+
except (EOFError, KeyboardInterrupt):
|
|
51
|
+
print()
|
|
52
|
+
sys.exit(0)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def table(rows: list, headers: list, max_col: int = 38):
|
|
56
|
+
"""Print a simple fixed-width table."""
|
|
57
|
+
if not rows:
|
|
58
|
+
print(" (no results)")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
widths = [len(h) for h in headers]
|
|
62
|
+
for row in rows:
|
|
63
|
+
for i, cell in enumerate(row):
|
|
64
|
+
widths[i] = min(max(widths[i], len(str(cell))), max_col)
|
|
65
|
+
|
|
66
|
+
fmt = " " + " ".join(f"{{:<{w}}}" for w in widths)
|
|
67
|
+
sep = " " + " ".join("─" * w for w in widths)
|
|
68
|
+
|
|
69
|
+
print()
|
|
70
|
+
print(fmt.format(*headers))
|
|
71
|
+
print(sep)
|
|
72
|
+
for row in rows:
|
|
73
|
+
cells = []
|
|
74
|
+
for i, cell in enumerate(row):
|
|
75
|
+
s = str(cell) if cell is not None else "-"
|
|
76
|
+
cells.append(s[: widths[i] - 1] + "…" if len(s) > widths[i] else s)
|
|
77
|
+
print(fmt.format(*cells))
|
|
78
|
+
print()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Minimal HTTP client (stdlib only)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import urllib.error
|
|
5
|
+
import urllib.request
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def api(base_url: str, method: str, path: str, body=None, content_type: str = "application/json"):
|
|
9
|
+
data = None
|
|
10
|
+
if body is not None:
|
|
11
|
+
data = body.encode() if isinstance(body, str) else json.dumps(body).encode()
|
|
12
|
+
req = urllib.request.Request(f"{base_url}{path}", data=data, method=method)
|
|
13
|
+
req.add_header("Content-Type", content_type)
|
|
14
|
+
try:
|
|
15
|
+
with urllib.request.urlopen(req) as resp:
|
|
16
|
+
return resp.status, json.loads(resp.read())
|
|
17
|
+
except urllib.error.HTTPError as e:
|
|
18
|
+
raw = e.read()
|
|
19
|
+
try:
|
|
20
|
+
return e.code, json.loads(raw)
|
|
21
|
+
except Exception:
|
|
22
|
+
return e.code, {"error": raw.decode()}
|
|
23
|
+
except urllib.error.URLError as e:
|
|
24
|
+
return None, {"error": str(e.reason)}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Interactive setup wizard: project creation, transform upload, datasource registration, sync."""
|
|
2
|
+
|
|
3
|
+
import importlib.resources
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from gigaflow import _config, _fmt
|
|
7
|
+
from gigaflow._http import api
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _load_default_transform() -> str:
|
|
11
|
+
"""Load the built-in Arize Phoenix transform config from the package."""
|
|
12
|
+
ref = importlib.resources.files("gigaflow.transforms").joinpath("arize_phoenix.yml")
|
|
13
|
+
return ref.read_text(encoding="utf-8")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_env_file(path: str) -> dict:
|
|
17
|
+
"""Parse a .env-style file and return key-value pairs.
|
|
18
|
+
|
|
19
|
+
Supports comments (#), blank lines, and optionally quoted values.
|
|
20
|
+
"""
|
|
21
|
+
env: dict[str, str] = {}
|
|
22
|
+
try:
|
|
23
|
+
with open(path) as f:
|
|
24
|
+
for line in f:
|
|
25
|
+
line = line.strip()
|
|
26
|
+
if not line or line.startswith("#"):
|
|
27
|
+
continue
|
|
28
|
+
if "=" not in line:
|
|
29
|
+
continue
|
|
30
|
+
key, _, value = line.partition("=")
|
|
31
|
+
key = key.strip()
|
|
32
|
+
value = value.strip()
|
|
33
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
|
|
34
|
+
value = value[1:-1]
|
|
35
|
+
if key:
|
|
36
|
+
env[key] = value
|
|
37
|
+
except OSError as e:
|
|
38
|
+
_fmt.fail(f"Could not read env file: {e}")
|
|
39
|
+
return env
|
|
40
|
+
|
|
41
|
+
ARIZE_TRANSFORM_YAML = _load_default_transform()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def check_backend(base_url: str) -> bool:
|
|
45
|
+
status, resp = api(base_url, "GET", "/health")
|
|
46
|
+
if status is None:
|
|
47
|
+
_fmt.fail(f"Could not reach gigaflow backend at {base_url}")
|
|
48
|
+
_fmt.info("Make sure the backend is running: cd backend && make run")
|
|
49
|
+
return False
|
|
50
|
+
if status != 200:
|
|
51
|
+
_fmt.fail(f"Backend returned {status}: {resp}")
|
|
52
|
+
return False
|
|
53
|
+
_fmt.ok(f"Backend reachable at {base_url}")
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def create_project(base_url: str, name: str) -> str | None:
|
|
58
|
+
status, resp = api(base_url, "POST", "/projects/", {"name": name})
|
|
59
|
+
if status != 200:
|
|
60
|
+
_fmt.fail(f"Failed to create project ({status}): {resp}")
|
|
61
|
+
return None
|
|
62
|
+
project_id = resp["project_id"]
|
|
63
|
+
_fmt.ok(f"Project created: {name}")
|
|
64
|
+
_fmt.info(f"project_id: {project_id}")
|
|
65
|
+
return project_id
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def upload_transform(base_url: str, project_id: str, yaml_content: str = ARIZE_TRANSFORM_YAML) -> bool:
|
|
69
|
+
status, resp = api(
|
|
70
|
+
base_url, "PUT", f"/projects/{project_id}/transform",
|
|
71
|
+
yaml_content, content_type="text/plain",
|
|
72
|
+
)
|
|
73
|
+
if status != 200:
|
|
74
|
+
_fmt.fail(f"Failed to upload transform config ({status}): {resp}")
|
|
75
|
+
return False
|
|
76
|
+
primitives = list(resp.get("transform_config", {}).get("primitives", {}).keys())
|
|
77
|
+
_fmt.ok("Transform config uploaded")
|
|
78
|
+
_fmt.info(f"primitives: {', '.join(primitives)}")
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def register_datasource(base_url: str, project_id: str, connection_url: str, source_table: str) -> str | None:
|
|
83
|
+
status, resp = api(base_url, "POST", "/datasources/", {
|
|
84
|
+
"project_id": project_id,
|
|
85
|
+
"name": "arize-phoenix",
|
|
86
|
+
"connection_url": connection_url,
|
|
87
|
+
"source_table": source_table,
|
|
88
|
+
})
|
|
89
|
+
if status != 200:
|
|
90
|
+
_fmt.fail(f"Failed to register datasource ({status}): {resp}")
|
|
91
|
+
return None
|
|
92
|
+
datasource_id = resp["datasource_id"]
|
|
93
|
+
_fmt.ok("Datasource registered")
|
|
94
|
+
_fmt.info(f"datasource_id: {datasource_id}")
|
|
95
|
+
return datasource_id
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def do_sync(base_url: str, datasource_id: str) -> tuple[int, int] | None:
|
|
99
|
+
status, resp = api(base_url, "POST", f"/datasources/{datasource_id}/sync")
|
|
100
|
+
if status != 200:
|
|
101
|
+
_fmt.fail(f"Sync failed ({status}): {resp.get('detail', resp)}")
|
|
102
|
+
detail = str(resp.get("detail", ""))
|
|
103
|
+
if "connect" in detail.lower() or status == 502:
|
|
104
|
+
_fmt.info("Could not connect to the source database.")
|
|
105
|
+
_fmt.info("If Arize is running in Docker, try 'host.docker.internal' as the host.")
|
|
106
|
+
return None
|
|
107
|
+
synced_traces = resp.get("synced_traces", 0)
|
|
108
|
+
synced_spans = resp.get("synced_spans", 0)
|
|
109
|
+
_fmt.ok(f"Sync complete: {synced_traces} trace(s), {synced_spans} span(s)")
|
|
110
|
+
return synced_traces, synced_spans
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def run_wizard(base_url: str) -> dict | None:
|
|
114
|
+
"""
|
|
115
|
+
Interactive wizard. Returns saved config dict on success, None on failure.
|
|
116
|
+
"""
|
|
117
|
+
_fmt.header("GigaFlow Setup Wizard")
|
|
118
|
+
|
|
119
|
+
# ── Env file (optional) ───────────────────────────────────────────────────
|
|
120
|
+
env_path = _fmt.prompt("Path to gigaflow.env (leave blank to enter values manually)")
|
|
121
|
+
if env_path:
|
|
122
|
+
env = load_env_file(env_path)
|
|
123
|
+
if env:
|
|
124
|
+
_fmt.ok(f"Loaded env file: {env_path}")
|
|
125
|
+
else:
|
|
126
|
+
env = {}
|
|
127
|
+
|
|
128
|
+
# ── Step 1: backend ───────────────────────────────────────────────────────
|
|
129
|
+
_fmt.section("Step 1: GigaFlow backend")
|
|
130
|
+
if not check_backend(base_url):
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
# ── Step 2: project ───────────────────────────────────────────────────────
|
|
134
|
+
_fmt.section("Step 2: Project")
|
|
135
|
+
project_name = _fmt.prompt("Project name", env.get("GIGAFLOW_PROJECT_NAME", "arize-phoenix-project"))
|
|
136
|
+
project_id = create_project(base_url, project_name)
|
|
137
|
+
if not project_id:
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
transform_path = _fmt.prompt(
|
|
141
|
+
"Path to transform.yml (leave blank for built-in Arize Phoenix config)",
|
|
142
|
+
env.get("GIGAFLOW_TRANSFORM_YML", ""),
|
|
143
|
+
)
|
|
144
|
+
if transform_path:
|
|
145
|
+
try:
|
|
146
|
+
with open(transform_path) as f:
|
|
147
|
+
yaml_content = f.read()
|
|
148
|
+
_fmt.ok(f"Loaded transform file: {transform_path}")
|
|
149
|
+
except OSError as e:
|
|
150
|
+
_fmt.fail(f"Could not read transform file: {e}")
|
|
151
|
+
return None
|
|
152
|
+
else:
|
|
153
|
+
yaml_content = ARIZE_TRANSFORM_YAML
|
|
154
|
+
_fmt.info("Using built-in Arize Phoenix transform config")
|
|
155
|
+
|
|
156
|
+
if not upload_transform(base_url, project_id, yaml_content):
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
# ── Step 3: Arize Phoenix DB ──────────────────────────────────────────────
|
|
160
|
+
_fmt.section("Step 3: Arize Phoenix database")
|
|
161
|
+
print()
|
|
162
|
+
print(" Enter the connection details for the PostgreSQL database")
|
|
163
|
+
print(" that Arize Phoenix writes to.")
|
|
164
|
+
print()
|
|
165
|
+
print(" Tip: if GigaFlow is running in Docker (the default),")
|
|
166
|
+
print(" use 'host.docker.internal' to reach the host machine.")
|
|
167
|
+
print()
|
|
168
|
+
print(" Find the Arize DB port with:")
|
|
169
|
+
print(" docker ps --filter name=arize_agent_example-db --format '{{.Ports}}'")
|
|
170
|
+
print()
|
|
171
|
+
|
|
172
|
+
host = _fmt.prompt("Host", env.get("GIGAFLOW_DB_HOST", "host.docker.internal"))
|
|
173
|
+
port = _fmt.prompt("Port", env.get("GIGAFLOW_DB_PORT", ""), required=True)
|
|
174
|
+
user = _fmt.prompt("User", env.get("GIGAFLOW_DB_USER", "postgres"))
|
|
175
|
+
|
|
176
|
+
if env.get("GIGAFLOW_DB_PASSWORD"):
|
|
177
|
+
password = env["GIGAFLOW_DB_PASSWORD"]
|
|
178
|
+
_fmt.info("Password: [from env file]")
|
|
179
|
+
else:
|
|
180
|
+
password = _fmt.prompt_password("Password")
|
|
181
|
+
|
|
182
|
+
db = _fmt.prompt("Database", env.get("GIGAFLOW_DB_NAME", "postgres"))
|
|
183
|
+
table = _fmt.prompt("Source table", env.get("GIGAFLOW_DB_TABLE", "spans"))
|
|
184
|
+
|
|
185
|
+
connection_url = f"postgresql://{user}:{password}@{host}:{port}/{db}"
|
|
186
|
+
|
|
187
|
+
# ── Step 4: register + sync ───────────────────────────────────────────────
|
|
188
|
+
_fmt.section("Step 4: Register datasource & sync")
|
|
189
|
+
datasource_id = register_datasource(base_url, project_id, connection_url, table)
|
|
190
|
+
if not datasource_id:
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
result = do_sync(base_url, datasource_id)
|
|
194
|
+
if result is None:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
synced_traces, _ = result
|
|
198
|
+
if synced_traces > 0:
|
|
199
|
+
_show_span_preview(base_url, project_id)
|
|
200
|
+
|
|
201
|
+
config: dict = {
|
|
202
|
+
"backend_url": base_url,
|
|
203
|
+
"project_id": project_id,
|
|
204
|
+
"datasource_id": datasource_id,
|
|
205
|
+
}
|
|
206
|
+
_config.save(config)
|
|
207
|
+
_fmt.ok(f"Configuration saved to {_config.CONFIG_PATH}")
|
|
208
|
+
return config
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _show_span_preview(base_url: str, project_id: str):
|
|
212
|
+
status, resp = api(base_url, "GET", f"/traces/?project_id={project_id}")
|
|
213
|
+
if status != 200:
|
|
214
|
+
return
|
|
215
|
+
traces = resp.get("traces", [])
|
|
216
|
+
if not traces:
|
|
217
|
+
return
|
|
218
|
+
trace_id = traces[0]["trace_id"]
|
|
219
|
+
status, resp = api(base_url, "GET", f"/traces/{trace_id}/spans")
|
|
220
|
+
if status != 200:
|
|
221
|
+
return
|
|
222
|
+
spans = resp if isinstance(resp, list) else resp.get("spans", [])
|
|
223
|
+
classified = [s for s in spans if s.get("primitive_type")]
|
|
224
|
+
unclassified = [s for s in spans if not s.get("primitive_type")]
|
|
225
|
+
_fmt.info(f"Sample trace — {len(classified)} classified, {len(unclassified)} unclassified")
|
|
226
|
+
for ptype in ["llm_call", "tool_invocation", "user_input"]:
|
|
227
|
+
match = next((s for s in spans if s.get("primitive_type") == ptype), None)
|
|
228
|
+
if match:
|
|
229
|
+
pd = match.get("primitive_data") or {}
|
|
230
|
+
_fmt.info(f" {ptype}: {pd}")
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gigaflow — CLI entry point.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
gigaflow run aif [TRACE_ID] Run AIF analysis (interactive if TRACE_ID omitted)
|
|
6
|
+
gigaflow setup Configure GigaFlow with an Arize Phoenix datasource
|
|
7
|
+
gigaflow sync Re-sync traces from the configured datasource
|
|
8
|
+
gigaflow traces List all traces (auto-syncs first)
|
|
9
|
+
gigaflow spans <trace_id> List spans for a trace (auto-syncs first)
|
|
10
|
+
gigaflow inspect <trace_id> Visualize a trace (orchestration graph + full span data)
|
|
11
|
+
gigaflow projects List all projects
|
|
12
|
+
gigaflow config show Show saved configuration
|
|
13
|
+
gigaflow config clear Clear saved configuration
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
from gigaflow import _config
|
|
21
|
+
from gigaflow._setup import load_env_file
|
|
22
|
+
from gigaflow.commands import config, inspect, projects, run, setup, traces
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
26
|
+
parser = argparse.ArgumentParser(
|
|
27
|
+
prog="gigaflow",
|
|
28
|
+
description="GigaFlow CLI — connect Arize Phoenix traces to GigaFlow and analyze them.",
|
|
29
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
30
|
+
epilog="""
|
|
31
|
+
examples:
|
|
32
|
+
gigaflow run aif
|
|
33
|
+
gigaflow run aif <trace_id>
|
|
34
|
+
gigaflow setup
|
|
35
|
+
gigaflow traces
|
|
36
|
+
gigaflow spans <trace_id>
|
|
37
|
+
gigaflow inspect <trace_id>
|
|
38
|
+
gigaflow inspect <trace_id> --cli
|
|
39
|
+
gigaflow inspect <trace_id> --port 8080
|
|
40
|
+
gigaflow sync
|
|
41
|
+
gigaflow config show
|
|
42
|
+
gigaflow config clear
|
|
43
|
+
""",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--backend",
|
|
47
|
+
metavar="URL",
|
|
48
|
+
default=None,
|
|
49
|
+
help="GigaFlow API base URL (default: from config or http://localhost:8000/api/v1)",
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"--env-file",
|
|
53
|
+
metavar="PATH",
|
|
54
|
+
default=None,
|
|
55
|
+
help="Path to gigaflow.env (default: auto-detects gigaflow.env in current directory)",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
sub = parser.add_subparsers(dest="command", metavar="<command>")
|
|
59
|
+
run.register(sub)
|
|
60
|
+
setup.register(sub)
|
|
61
|
+
traces.register(sub)
|
|
62
|
+
inspect.register(sub)
|
|
63
|
+
projects.register(sub)
|
|
64
|
+
config.register(sub)
|
|
65
|
+
|
|
66
|
+
return parser
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main():
|
|
70
|
+
parser = _build_parser()
|
|
71
|
+
args = parser.parse_args()
|
|
72
|
+
|
|
73
|
+
if not hasattr(args, "func"):
|
|
74
|
+
parser.print_help()
|
|
75
|
+
sys.exit(0)
|
|
76
|
+
|
|
77
|
+
# Load gigaflow.env and inject keys into os.environ (without overriding existing vars)
|
|
78
|
+
env_file = args.env_file or ("gigaflow.env" if os.path.exists("gigaflow.env") else None)
|
|
79
|
+
if env_file:
|
|
80
|
+
for key, value in load_env_file(env_file).items():
|
|
81
|
+
os.environ.setdefault(key, value)
|
|
82
|
+
|
|
83
|
+
cfg = _config.load()
|
|
84
|
+
base_url = (args.backend or cfg.get("backend_url") or "http://localhost:8000/api/v1").rstrip("/")
|
|
85
|
+
|
|
86
|
+
args.func(args, base_url)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command modules — one file per top-level command group."""
|