sovara 0.2.1__tar.gz → 0.2.3__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.
- {sovara-0.2.1 → sovara-0.2.3}/PKG-INFO +3 -1
- {sovara-0.2.1 → sovara-0.2.3}/pyproject.toml +6 -2
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/assets/SKILL.md +34 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/cli/so_record.py +10 -1
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/cli/so_server.py +58 -38
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/cli/so_tool.py +134 -2
- sovara-0.2.3/src/sovara/common/runtime.py +15 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/agent_runner.py +43 -5
- sovara-0.2.3/src/sovara/runner/run_log_capture.py +205 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/app.py +29 -1
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/runs.py +14 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/sqlite/runs.py +15 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/inference_server.py +17 -8
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/handlers/__init__.py +2 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/handlers/cli_handlers.py +19 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/handlers/runner_handlers.py +22 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/routes/runner.py +18 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/routes/ui.py +1 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/routes/ui_runs.py +9 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/state.py +7 -0
- sovara-0.2.3/src/sovara/server/web_app_assets.py +68 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara.egg-info/PKG-INFO +3 -1
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara.egg-info/SOURCES.txt +3 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara.egg-info/requires.txt +3 -0
- {sovara-0.2.1 → sovara-0.2.3}/LICENSE +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/README.md +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/docs/release/PYPI_DESC.md +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/setup.cfg +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/__init__.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/assets/__init__.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/cli/__init__.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/cli/so_config.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/common/__init__.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/common/config.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/common/constants.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/common/custom_metrics.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/common/logger.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/common/project.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/common/user.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/common/utils.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/__init__.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/context_manager.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/__init__.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/api_parser.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/api_parsers/__init__.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/api_parsers/aiohttp_api_parser.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/api_parsers/claude_sdk_api_parser.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/api_parsers/httpx_api_parser.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/api_parsers/mcp_api_parser.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/api_parsers/requests_api_parser.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/api_parsers/sse_utils.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/api_parsers/urllib3_api_parser.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/apply_monkey_patches.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/async_streaming.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/edit_io_filters.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/endpoint_whitelist.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/node_naming.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/patches/__init__.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/patches/aiohttp_patch.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/patches/claude_sdk_patch.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/patches/httpx_patch.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/patches/mcp_patches.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/patches/randomness_patch.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/patches/requests_patch.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/patches/urllib3_patch.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/patching_utils.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/priors.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/priors_pipeline.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/string_matching.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/__init__.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/__init__.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/_shared.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/llm_calls.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/manager.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/priors.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/projects.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/sqlite/__init__.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/sqlite/connection.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/sqlite/llm_calls.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/sqlite/priors.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/sqlite/projects.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/sqlite/schema.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/sqlite/users.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/users.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/__init__.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/cancel.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/main.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/tools/__init__.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/tools/ask_step.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/tools/edit_content.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/tools/get_step_overview.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/tools/get_step_snapshot.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/tools/get_trace_overview.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/tools/search.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/tools/summarize_trace.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/tools/verify.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/utils/__init__.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/utils/content_utils.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/utils/context.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/utils/edit_persist.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/utils/step_content_view.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/utils/step_ids.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/utils/trace.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_models.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/handlers/handler_utils.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/handlers/ui_handlers.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/llm_backend.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/llm_provider_catalog.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/llm_settings.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/package_version.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/prior_display.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/__init__.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/constants.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/events.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/llm/__init__.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/llm/lesson_restructurer.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/llm/lesson_retriever.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/llm/lesson_summarizer.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/llm/lesson_validator.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/llm_client.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/logger.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/routes.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/storage.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_scope.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/routes/__init__.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/routes/events.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/routes/internal.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/routes/ui_common.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/routes/ui_projects.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/routes/ui_trace_chat.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/services/__init__.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/services/trace_chat_service.py +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara.egg-info/dependency_links.txt +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara.egg-info/entry_points.txt +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/src/sovara.egg-info/top_level.txt +0 -0
- {sovara-0.2.1 → sovara-0.2.3}/tests/test_graph_payloads.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sovara
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: A development tool for LLM call visualization and debugging
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -33,6 +33,8 @@ Requires-Dist: langchain; extra == "dev"
|
|
|
33
33
|
Requires-Dist: langchain-anthropic; extra == "dev"
|
|
34
34
|
Requires-Dist: langchain-openai; extra == "dev"
|
|
35
35
|
Requires-Dist: websocket-client; extra == "dev"
|
|
36
|
+
Provides-Extra: desktop
|
|
37
|
+
Requires-Dist: pyinstaller>=6.11; extra == "desktop"
|
|
36
38
|
Provides-Extra: examples
|
|
37
39
|
Requires-Dist: openai; extra == "examples"
|
|
38
40
|
Requires-Dist: anthropic; extra == "examples"
|
|
@@ -5,7 +5,7 @@ requires = ["setuptools"]
|
|
|
5
5
|
build-backend = "setuptools.build_meta"
|
|
6
6
|
|
|
7
7
|
[tool.setuptools.package-data]
|
|
8
|
-
"sovara.assets" = ["SKILL.md"]
|
|
8
|
+
"sovara.assets" = ["SKILL.md", "web_app/**/*"]
|
|
9
9
|
|
|
10
10
|
[tool.setuptools]
|
|
11
11
|
package-dir = {"" = "src"}
|
|
@@ -26,7 +26,7 @@ line-length = 100
|
|
|
26
26
|
|
|
27
27
|
[project]
|
|
28
28
|
name = "sovara"
|
|
29
|
-
version = "0.2.
|
|
29
|
+
version = "0.2.3"
|
|
30
30
|
description = "A development tool for LLM call visualization and debugging"
|
|
31
31
|
readme = "docs/release/PYPI_DESC.md"
|
|
32
32
|
requires-python = ">=3.10"
|
|
@@ -65,6 +65,10 @@ dev = [
|
|
|
65
65
|
"websocket-client"
|
|
66
66
|
]
|
|
67
67
|
|
|
68
|
+
desktop = [
|
|
69
|
+
"pyinstaller>=6.11",
|
|
70
|
+
]
|
|
71
|
+
|
|
68
72
|
examples = [
|
|
69
73
|
"openai",
|
|
70
74
|
"anthropic",
|
|
@@ -208,6 +208,40 @@ This will produce the full result (no preview) of the specific keys that match t
|
|
|
208
208
|
}
|
|
209
209
|
```
|
|
210
210
|
|
|
211
|
+
### Inspect persisted logs
|
|
212
|
+
If you want to inspect the captured stdout/stderr transcript for a run, use the `logs` command. Unlike `probe` and `runs`, this command prints plain text rather than JSON.
|
|
213
|
+
|
|
214
|
+
The tool is generally structured like this.
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
usage: so-cli logs [-h] [--tail TAIL] [--grep GREP] [--context CONTEXT] [--line-numbers] run_id
|
|
218
|
+
|
|
219
|
+
positional arguments:
|
|
220
|
+
run_id Run ID to fetch logs for
|
|
221
|
+
|
|
222
|
+
options:
|
|
223
|
+
-h, --help show this help message and exit
|
|
224
|
+
--tail TAIL Show only the last N displayed lines
|
|
225
|
+
--grep GREP Filter displayed lines using a regex pattern
|
|
226
|
+
--context CONTEXT Show N lines of context around each grep match
|
|
227
|
+
--line-numbers Prefix displayed lines with original line numbers
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`run_id` accepts a full UUID or any unambiguous prefix, just like `probe`.
|
|
231
|
+
|
|
232
|
+
Examples:
|
|
233
|
+
To inspect the last 40 lines of a run, run
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
uv run so-cli logs b6aaf796 --tail 40
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
To search a large log for cache misses with nearby context and original line numbers, run
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
uv run so-cli logs b6aaf796 --grep "Cache miss" --context 2 --line-numbers
|
|
243
|
+
```
|
|
244
|
+
|
|
211
245
|
### List and manage runs
|
|
212
246
|
You can manage past runs that you did with the `runs` command, which is structured in the following way
|
|
213
247
|
|
|
@@ -3,7 +3,6 @@ import time
|
|
|
3
3
|
import yaml
|
|
4
4
|
from argparse import ArgumentParser, REMAINDER
|
|
5
5
|
from sovara.common.constants import SOVARA_CONFIG
|
|
6
|
-
from sovara.runner.agent_runner import AgentRunner
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
def launch_command_parser():
|
|
@@ -32,6 +31,13 @@ def launch_command_parser():
|
|
|
32
31
|
help="Change each process to interpret the launch script as a Python module, executing with the same behavior as 'python -m'.",
|
|
33
32
|
)
|
|
34
33
|
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"-u",
|
|
36
|
+
"--unbuffered",
|
|
37
|
+
action="store_true",
|
|
38
|
+
help="Run with unbuffered stdout/stderr, like 'python -u'.",
|
|
39
|
+
)
|
|
40
|
+
|
|
35
41
|
parser.add_argument(
|
|
36
42
|
"script_path",
|
|
37
43
|
type=str,
|
|
@@ -71,12 +77,15 @@ def _validate_launch_command(args):
|
|
|
71
77
|
|
|
72
78
|
|
|
73
79
|
def launch_command(args):
|
|
80
|
+
from sovara.runner.agent_runner import AgentRunner
|
|
81
|
+
|
|
74
82
|
args = _validate_launch_command(args)
|
|
75
83
|
|
|
76
84
|
agent_runner = AgentRunner(
|
|
77
85
|
script_path=args.script_path,
|
|
78
86
|
script_args=args.script_args,
|
|
79
87
|
is_module_execution=args.module,
|
|
88
|
+
unbuffered=args.unbuffered,
|
|
80
89
|
run_name=args.run_name,
|
|
81
90
|
)
|
|
82
91
|
agent_runner.run()
|
|
@@ -12,9 +12,10 @@ if package_parent not in sys.path:
|
|
|
12
12
|
import time
|
|
13
13
|
import subprocess
|
|
14
14
|
import json
|
|
15
|
-
from argparse import ArgumentParser
|
|
15
|
+
from argparse import ArgumentParser, SUPPRESS
|
|
16
16
|
|
|
17
17
|
from sovara.common.logger import logger, create_file_logger
|
|
18
|
+
from sovara.common.runtime import build_so_server_command
|
|
18
19
|
|
|
19
20
|
from sovara.common.constants import (
|
|
20
21
|
MAIN_SERVER_LOG,
|
|
@@ -22,6 +23,7 @@ from sovara.common.constants import (
|
|
|
22
23
|
INFERENCE_SERVER_LOG,
|
|
23
24
|
MAIN_SERVER_STARTUP_LOCK,
|
|
24
25
|
HOST,
|
|
26
|
+
INFERENCE_PORT,
|
|
25
27
|
PORT,
|
|
26
28
|
SHUTDOWN_WAIT,
|
|
27
29
|
)
|
|
@@ -127,7 +129,7 @@ def launch_daemon_server() -> bool:
|
|
|
127
129
|
try:
|
|
128
130
|
with open(MAIN_SERVER_LOG, "a+") as log_f:
|
|
129
131
|
subprocess.Popen(
|
|
130
|
-
|
|
132
|
+
build_so_server_command("_serve", "--host", HOST, "--port", str(PORT)),
|
|
131
133
|
close_fds=True,
|
|
132
134
|
start_new_session=True,
|
|
133
135
|
stdin=subprocess.DEVNULL,
|
|
@@ -160,12 +162,61 @@ def server_command_parser():
|
|
|
160
162
|
"clear-logs",
|
|
161
163
|
"_clear_cache",
|
|
162
164
|
"_serve",
|
|
165
|
+
"_serve-inference",
|
|
163
166
|
],
|
|
164
167
|
help="The command to execute for the server.",
|
|
165
168
|
)
|
|
169
|
+
parser.add_argument("--host", default=None, help=SUPPRESS)
|
|
170
|
+
parser.add_argument("--port", type=int, default=None, help=SUPPRESS)
|
|
166
171
|
return parser
|
|
167
172
|
|
|
168
173
|
|
|
174
|
+
def _serve_main_server(host: str, port: int) -> None:
|
|
175
|
+
_server_logger.info(f"Imports completed in {_time.time() - _import_start:.2f}s")
|
|
176
|
+
|
|
177
|
+
# Save Python executable path to config for VS Code extension to use
|
|
178
|
+
from sovara.common.constants import SOVARA_CONFIG
|
|
179
|
+
from sovara.common.config import Config
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
config = Config.from_yaml_file(SOVARA_CONFIG)
|
|
183
|
+
if config.python_executable != sys.executable:
|
|
184
|
+
config.python_executable = sys.executable
|
|
185
|
+
config.to_yaml_file(SOVARA_CONFIG)
|
|
186
|
+
_server_logger.info(f"Saved python_executable: {sys.executable}")
|
|
187
|
+
except Exception as e:
|
|
188
|
+
_server_logger.warning(f"Could not save python_executable: {e}")
|
|
189
|
+
|
|
190
|
+
import uvicorn
|
|
191
|
+
from sovara.server.app import create_app
|
|
192
|
+
|
|
193
|
+
_start = _time.time()
|
|
194
|
+
app = create_app()
|
|
195
|
+
_server_logger.info(f"FastAPI app created in {_time.time() - _start:.2f}s")
|
|
196
|
+
|
|
197
|
+
# Use Config+Server instead of uvicorn.run() so we can set
|
|
198
|
+
# server.should_exit = True for clean programmatic shutdown.
|
|
199
|
+
uv_config = uvicorn.Config(app, host=host, port=port, log_level="warning")
|
|
200
|
+
uv_server = uvicorn.Server(uv_config)
|
|
201
|
+
|
|
202
|
+
# Store reference on app so lifespan can wire it into ServerState.
|
|
203
|
+
app.state.uvicorn_server = uv_server
|
|
204
|
+
try:
|
|
205
|
+
uv_server.run()
|
|
206
|
+
finally:
|
|
207
|
+
try:
|
|
208
|
+
_stop_child_servers()
|
|
209
|
+
except Exception as exc:
|
|
210
|
+
_server_logger.warning(f"Could not stop child servers during shutdown: {exc}")
|
|
211
|
+
_release_startup_lock()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _serve_inference_server(host: str, port: int) -> None:
|
|
215
|
+
from sovara.server.graph_analysis.inference_server import serve
|
|
216
|
+
|
|
217
|
+
serve(host=host, port=port)
|
|
218
|
+
|
|
219
|
+
|
|
169
220
|
def execute_server_command(args):
|
|
170
221
|
if args.command == "start":
|
|
171
222
|
if _is_server_running():
|
|
@@ -250,43 +301,12 @@ def execute_server_command(args):
|
|
|
250
301
|
return
|
|
251
302
|
|
|
252
303
|
elif args.command == "_serve":
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
# Save Python executable path to config for VS Code extension to use
|
|
256
|
-
from sovara.common.constants import SOVARA_CONFIG
|
|
257
|
-
from sovara.common.config import Config
|
|
258
|
-
|
|
259
|
-
try:
|
|
260
|
-
config = Config.from_yaml_file(SOVARA_CONFIG)
|
|
261
|
-
if config.python_executable != sys.executable:
|
|
262
|
-
config.python_executable = sys.executable
|
|
263
|
-
config.to_yaml_file(SOVARA_CONFIG)
|
|
264
|
-
_server_logger.info(f"Saved python_executable: {sys.executable}")
|
|
265
|
-
except Exception as e:
|
|
266
|
-
_server_logger.warning(f"Could not save python_executable: {e}")
|
|
267
|
-
|
|
268
|
-
import uvicorn
|
|
269
|
-
from sovara.server.app import create_app
|
|
270
|
-
|
|
271
|
-
_start = _time.time()
|
|
272
|
-
app = create_app()
|
|
273
|
-
_server_logger.info(f"FastAPI app created in {_time.time() - _start:.2f}s")
|
|
274
|
-
|
|
275
|
-
# Use Config+Server instead of uvicorn.run() so we can set
|
|
276
|
-
# server.should_exit = True for clean programmatic shutdown.
|
|
277
|
-
uv_config = uvicorn.Config(app, host=HOST, port=PORT, log_level="warning")
|
|
278
|
-
uv_server = uvicorn.Server(uv_config)
|
|
304
|
+
_serve_main_server(args.host or HOST, args.port or PORT)
|
|
305
|
+
return
|
|
279
306
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
uv_server.run()
|
|
284
|
-
finally:
|
|
285
|
-
try:
|
|
286
|
-
_stop_child_servers()
|
|
287
|
-
except Exception as exc:
|
|
288
|
-
_server_logger.warning(f"Could not stop child servers during shutdown: {exc}")
|
|
289
|
-
_release_startup_lock()
|
|
307
|
+
elif args.command == "_serve-inference":
|
|
308
|
+
_serve_inference_server(args.host or HOST, args.port or INFERENCE_PORT)
|
|
309
|
+
return
|
|
290
310
|
|
|
291
311
|
|
|
292
312
|
def main():
|
|
@@ -2,6 +2,7 @@ import atexit
|
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
4
|
import json
|
|
5
|
+
import re
|
|
5
6
|
import shlex
|
|
6
7
|
import shutil
|
|
7
8
|
import uuid
|
|
@@ -190,6 +191,77 @@ def _fetch_all_runs() -> list[dict]:
|
|
|
190
191
|
return runs
|
|
191
192
|
|
|
192
193
|
|
|
194
|
+
def format_log_output(
|
|
195
|
+
log_text: str,
|
|
196
|
+
*,
|
|
197
|
+
tail: int | None = None,
|
|
198
|
+
grep: str | None = None,
|
|
199
|
+
context: int = 0,
|
|
200
|
+
line_numbers: bool = False,
|
|
201
|
+
) -> str:
|
|
202
|
+
"""Render persisted run logs with optional filtering and line-number formatting."""
|
|
203
|
+
if tail is not None and tail < 0:
|
|
204
|
+
raise ValueError("--tail must be greater than or equal to 0")
|
|
205
|
+
if context < 0:
|
|
206
|
+
raise ValueError("--context must be greater than or equal to 0")
|
|
207
|
+
|
|
208
|
+
if grep is None and tail is None and context == 0 and not line_numbers:
|
|
209
|
+
return log_text
|
|
210
|
+
|
|
211
|
+
lines = log_text.splitlines()
|
|
212
|
+
entries: list[tuple[int, str] | None]
|
|
213
|
+
|
|
214
|
+
if grep:
|
|
215
|
+
try:
|
|
216
|
+
pattern = re.compile(grep)
|
|
217
|
+
except re.error as exc:
|
|
218
|
+
raise ValueError(f"Invalid regex: {exc}") from exc
|
|
219
|
+
|
|
220
|
+
ranges: list[tuple[int, int]] = []
|
|
221
|
+
for index, line in enumerate(lines):
|
|
222
|
+
if not pattern.search(line):
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
start = max(0, index - context)
|
|
226
|
+
end = min(len(lines) - 1, index + context)
|
|
227
|
+
if ranges and start <= ranges[-1][1] + 1:
|
|
228
|
+
ranges[-1] = (ranges[-1][0], max(ranges[-1][1], end))
|
|
229
|
+
else:
|
|
230
|
+
ranges.append((start, end))
|
|
231
|
+
|
|
232
|
+
entries = []
|
|
233
|
+
for range_index, (start, end) in enumerate(ranges):
|
|
234
|
+
if range_index > 0:
|
|
235
|
+
entries.append(None)
|
|
236
|
+
for line_index in range(start, end + 1):
|
|
237
|
+
entries.append((line_index + 1, lines[line_index]))
|
|
238
|
+
else:
|
|
239
|
+
entries = [(index + 1, line) for index, line in enumerate(lines)]
|
|
240
|
+
|
|
241
|
+
if tail is not None:
|
|
242
|
+
entries = entries[-tail:] if tail > 0 else []
|
|
243
|
+
|
|
244
|
+
if not entries:
|
|
245
|
+
return ""
|
|
246
|
+
|
|
247
|
+
numbered_lines = [entry for entry in entries if entry is not None]
|
|
248
|
+
number_width = len(str(max((line_number for line_number, _line in numbered_lines), default=1)))
|
|
249
|
+
|
|
250
|
+
rendered_lines: list[str] = []
|
|
251
|
+
for entry in entries:
|
|
252
|
+
if entry is None:
|
|
253
|
+
rendered_lines.append("--")
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
line_number, line = entry
|
|
257
|
+
if line_numbers:
|
|
258
|
+
rendered_lines.append(f"{line_number:>{number_width}}: {line}")
|
|
259
|
+
else:
|
|
260
|
+
rendered_lines.append(line)
|
|
261
|
+
|
|
262
|
+
return "\n".join(rendered_lines) + "\n"
|
|
263
|
+
|
|
264
|
+
|
|
193
265
|
def _spawn_rerun(prepared: dict, timeout: float | None = None) -> dict:
|
|
194
266
|
"""
|
|
195
267
|
Spawn a rerun process for a run and block until completion.
|
|
@@ -311,11 +383,16 @@ def record_command(args) -> None:
|
|
|
311
383
|
|
|
312
384
|
atexit.register(cleanup_run_file)
|
|
313
385
|
|
|
314
|
-
cmd = [sys.executable
|
|
386
|
+
cmd = [sys.executable]
|
|
387
|
+
if args.unbuffered:
|
|
388
|
+
cmd.append("-u")
|
|
389
|
+
cmd.extend(["-m", "sovara.cli.so_record"])
|
|
315
390
|
if args.run_name:
|
|
316
391
|
cmd.extend(["--run-name", args.run_name])
|
|
317
392
|
if args.module:
|
|
318
393
|
cmd.append("-m")
|
|
394
|
+
if args.unbuffered:
|
|
395
|
+
cmd.append("-u")
|
|
319
396
|
cmd.append(args.script_path)
|
|
320
397
|
cmd.extend(args.script_args)
|
|
321
398
|
|
|
@@ -382,6 +459,27 @@ def probe_command(args) -> None:
|
|
|
382
459
|
output_json(result)
|
|
383
460
|
|
|
384
461
|
|
|
462
|
+
def logs_command(args) -> None:
|
|
463
|
+
"""Show the persisted log for a run."""
|
|
464
|
+
result = _server_request("GET", f"/ui/run/{args.run_id}/log")
|
|
465
|
+
if result.get("status") == "error":
|
|
466
|
+
output_json(result)
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
rendered = format_log_output(
|
|
470
|
+
result.get("log", ""),
|
|
471
|
+
tail=args.tail,
|
|
472
|
+
grep=args.grep,
|
|
473
|
+
context=args.context,
|
|
474
|
+
line_numbers=args.line_numbers,
|
|
475
|
+
)
|
|
476
|
+
except ValueError as exc:
|
|
477
|
+
output_json({"status": "error", "error": str(exc)})
|
|
478
|
+
|
|
479
|
+
sys.stdout.write(rendered)
|
|
480
|
+
sys.stdout.flush()
|
|
481
|
+
|
|
482
|
+
|
|
385
483
|
def runs_command(args) -> None:
|
|
386
484
|
"""List runs via the REST API."""
|
|
387
485
|
import re
|
|
@@ -965,7 +1063,7 @@ def create_parser() -> ArgumentParser:
|
|
|
965
1063
|
"""Create the argument parser with subcommands."""
|
|
966
1064
|
parser = ArgumentParser(
|
|
967
1065
|
prog="so-cli",
|
|
968
|
-
description="CLI for programmatic interaction with Sovara dataflow system.
|
|
1066
|
+
description="CLI for programmatic interaction with Sovara dataflow system. Most commands output JSON; `logs` outputs plain text.",
|
|
969
1067
|
)
|
|
970
1068
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
971
1069
|
|
|
@@ -980,6 +1078,11 @@ def create_parser() -> ArgumentParser:
|
|
|
980
1078
|
action="store_true",
|
|
981
1079
|
help="Run script_path as a Python module (like python -m)",
|
|
982
1080
|
)
|
|
1081
|
+
record.add_argument(
|
|
1082
|
+
"-u", "--unbuffered",
|
|
1083
|
+
action="store_true",
|
|
1084
|
+
help="Run with unbuffered stdout/stderr, like python -u",
|
|
1085
|
+
)
|
|
983
1086
|
record.add_argument("--run-name", help="Human-readable name for this run")
|
|
984
1087
|
record.add_argument(
|
|
985
1088
|
"--timeout",
|
|
@@ -1028,6 +1131,33 @@ def create_parser() -> ArgumentParser:
|
|
|
1028
1131
|
help="Filter keys using regex pattern on flattened keys (e.g., 'messages.*content'). Lists use index notation: content.0.hello",
|
|
1029
1132
|
)
|
|
1030
1133
|
|
|
1134
|
+
logs = subparsers.add_parser(
|
|
1135
|
+
"logs",
|
|
1136
|
+
help="Show a run's persisted logs",
|
|
1137
|
+
description="Show the persisted log transcript for a run with optional filtering.",
|
|
1138
|
+
)
|
|
1139
|
+
logs.add_argument("run_id", help="Run UUID or unambiguous prefix to fetch logs for")
|
|
1140
|
+
logs.add_argument(
|
|
1141
|
+
"--tail",
|
|
1142
|
+
type=int,
|
|
1143
|
+
help="Show only the last N displayed lines",
|
|
1144
|
+
)
|
|
1145
|
+
logs.add_argument(
|
|
1146
|
+
"--grep",
|
|
1147
|
+
help="Filter displayed lines using a regex pattern",
|
|
1148
|
+
)
|
|
1149
|
+
logs.add_argument(
|
|
1150
|
+
"--context",
|
|
1151
|
+
type=int,
|
|
1152
|
+
default=0,
|
|
1153
|
+
help="Show N lines of context around each grep match",
|
|
1154
|
+
)
|
|
1155
|
+
logs.add_argument(
|
|
1156
|
+
"--line-numbers",
|
|
1157
|
+
action="store_true",
|
|
1158
|
+
help="Prefix displayed lines with original line numbers",
|
|
1159
|
+
)
|
|
1160
|
+
|
|
1031
1161
|
# runs subcommand
|
|
1032
1162
|
runs = subparsers.add_parser(
|
|
1033
1163
|
"runs",
|
|
@@ -1356,6 +1486,8 @@ def main():
|
|
|
1356
1486
|
record_command(args)
|
|
1357
1487
|
elif args.command == "probe":
|
|
1358
1488
|
probe_command(args)
|
|
1489
|
+
elif args.command == "logs":
|
|
1490
|
+
logs_command(args)
|
|
1359
1491
|
elif args.command == "runs":
|
|
1360
1492
|
runs_command(args)
|
|
1361
1493
|
elif args.command == "edit-and-rerun":
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Helpers for launching Sovara subprocesses across source and frozen builds."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def is_frozen_runtime() -> bool:
|
|
9
|
+
return bool(getattr(sys, "frozen", False))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_so_server_command(*args: str) -> list[str]:
|
|
13
|
+
if is_frozen_runtime():
|
|
14
|
+
return [sys.executable, *args]
|
|
15
|
+
return [sys.executable, "-m", "sovara.cli.so_server", *args]
|
|
@@ -31,6 +31,7 @@ from sovara.runner.context_manager import (
|
|
|
31
31
|
)
|
|
32
32
|
from sovara.common.utils import set_server_url, http_post
|
|
33
33
|
from sovara.runner.monkey_patching.apply_monkey_patches import apply_all_monkey_patches
|
|
34
|
+
from sovara.runner.run_log_capture import RunLogCapture
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
def _log_error(context: str, exception: Exception) -> None:
|
|
@@ -128,12 +129,14 @@ class AgentRunner:
|
|
|
128
129
|
script_path: str,
|
|
129
130
|
script_args: List[str],
|
|
130
131
|
is_module_execution: bool,
|
|
132
|
+
unbuffered: bool = False,
|
|
131
133
|
sample_id: Optional[str] = None,
|
|
132
134
|
run_name: Optional[str] = None,
|
|
133
135
|
):
|
|
134
136
|
self.script_path = script_path
|
|
135
137
|
self.script_args = script_args
|
|
136
138
|
self.is_module_execution = is_module_execution
|
|
139
|
+
self.unbuffered = unbuffered
|
|
137
140
|
self.sample_id = sample_id
|
|
138
141
|
self.run_name = run_name
|
|
139
142
|
|
|
@@ -238,6 +241,7 @@ class AgentRunner:
|
|
|
238
241
|
"""Generate the appropriate command for restarting the script."""
|
|
239
242
|
original_command = " ".join(shlex.quote(arg) for arg in sys.argv)
|
|
240
243
|
python_executable = sys.executable
|
|
244
|
+
interpreter_flags = "-u " if self.unbuffered else ""
|
|
241
245
|
|
|
242
246
|
if not self._is_debugpy_session():
|
|
243
247
|
return f"{python_executable} {original_command}"
|
|
@@ -251,20 +255,36 @@ class AgentRunner:
|
|
|
251
255
|
if "launcher" in cmdline_str and "--" in parent_cmdline:
|
|
252
256
|
dash_index = parent_cmdline.index("--")
|
|
253
257
|
original_args = " ".join(shlex.quote(arg) for arg in parent_cmdline[dash_index + 1:])
|
|
254
|
-
return f"/usr/bin/env {python_executable} {original_args}"
|
|
258
|
+
return f"/usr/bin/env {python_executable} {interpreter_flags}{original_args}".strip()
|
|
255
259
|
|
|
256
260
|
if "-m" in parent_cmdline and "debugpy" in parent_cmdline:
|
|
257
261
|
if self.is_module_execution:
|
|
258
262
|
target_args = f"-m {self.script_path} {' '.join(shlex.quote(arg) for arg in self.script_args)}"
|
|
259
263
|
else:
|
|
260
264
|
target_args = f"{shlex.quote(self.script_path)} {' '.join(shlex.quote(arg) for arg in self.script_args)}"
|
|
261
|
-
return f"{python_executable} {target_args}"
|
|
265
|
+
return f"{python_executable} {interpreter_flags}{target_args}".strip()
|
|
262
266
|
|
|
263
267
|
if self.is_module_execution:
|
|
264
268
|
target_args = f"-m {self.script_path} {' '.join(shlex.quote(arg) for arg in self.script_args)}"
|
|
265
269
|
else:
|
|
266
270
|
target_args = f"{shlex.quote(self.script_path)} {' '.join(shlex.quote(arg) for arg in self.script_args)}"
|
|
267
|
-
return f"{python_executable} {target_args}"
|
|
271
|
+
return f"{python_executable} {interpreter_flags}{target_args}".strip()
|
|
272
|
+
|
|
273
|
+
def _apply_python_runtime_flags(self) -> None:
|
|
274
|
+
"""Apply interpreter-like runtime flags that matter for in-process execution."""
|
|
275
|
+
if not self.unbuffered:
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
os.environ["PYTHONUNBUFFERED"] = "1"
|
|
279
|
+
for stream_name in ("stdout", "stderr"):
|
|
280
|
+
stream = getattr(sys, stream_name, None)
|
|
281
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
282
|
+
if not callable(reconfigure):
|
|
283
|
+
continue
|
|
284
|
+
try:
|
|
285
|
+
reconfigure(line_buffering=True, write_through=True)
|
|
286
|
+
except ValueError:
|
|
287
|
+
logger.debug(f"Could not reconfigure sys.{stream_name} for unbuffered mode")
|
|
268
288
|
|
|
269
289
|
def _register_with_server(self) -> None:
|
|
270
290
|
"""Register with the server via HTTP POST."""
|
|
@@ -357,6 +377,23 @@ class AgentRunner:
|
|
|
357
377
|
traceback.print_exc()
|
|
358
378
|
return 1
|
|
359
379
|
|
|
380
|
+
def _append_run_log(self, log_chunk: str) -> None:
|
|
381
|
+
if not self.run_id or not log_chunk:
|
|
382
|
+
return
|
|
383
|
+
http_post("/runner/update-log", {
|
|
384
|
+
"run_id": self.run_id,
|
|
385
|
+
"log_text": log_chunk,
|
|
386
|
+
"replace": False,
|
|
387
|
+
}, timeout=2.0)
|
|
388
|
+
|
|
389
|
+
def _run_user_code_with_output_capture(self) -> int:
|
|
390
|
+
if not self.run_id:
|
|
391
|
+
return self._execute_user_code()
|
|
392
|
+
with RunLogCapture(
|
|
393
|
+
append_callback=self._append_run_log,
|
|
394
|
+
):
|
|
395
|
+
return self._execute_user_code()
|
|
396
|
+
|
|
360
397
|
def _run_debug_mode(self) -> int:
|
|
361
398
|
"""Run in debug mode with persistent restart loop."""
|
|
362
399
|
logger.info("[AgentRunner] Debug mode detected. Running with restart capability.")
|
|
@@ -368,7 +405,7 @@ class AgentRunner:
|
|
|
368
405
|
self._apply_runtime_setup()
|
|
369
406
|
first_run = False
|
|
370
407
|
|
|
371
|
-
exit_code = self.
|
|
408
|
+
exit_code = self._run_user_code_with_output_capture()
|
|
372
409
|
logger.info(f"[AgentRunner] Script completed with exit code {exit_code}. Waiting...")
|
|
373
410
|
|
|
374
411
|
while not self.shutdown_flag and not self.restart_event.is_set():
|
|
@@ -387,11 +424,12 @@ class AgentRunner:
|
|
|
387
424
|
def _run_normal_mode(self) -> int:
|
|
388
425
|
"""Run in normal mode (single execution)."""
|
|
389
426
|
self._apply_runtime_setup()
|
|
390
|
-
return self.
|
|
427
|
+
return self._run_user_code_with_output_capture()
|
|
391
428
|
|
|
392
429
|
def run(self) -> None:
|
|
393
430
|
"""Main entry point."""
|
|
394
431
|
try:
|
|
432
|
+
self._apply_python_runtime_flags()
|
|
395
433
|
self._setup_environment()
|
|
396
434
|
ensure_server_running()
|
|
397
435
|
|