uprobe 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uprobe/__init__.py +18 -0
- uprobe/__main__.py +17 -0
- uprobe/core/.readthedocs.yaml +53 -0
- uprobe/core/__init__.py +26 -0
- uprobe/core/agent/__init__.py +1 -0
- uprobe/core/agent/repl_bootstrap.py +193 -0
- uprobe/core/agent/session_manager.py +207 -0
- uprobe/core/agent/templates/DEFAULT_PROTOCOL.yaml +55 -0
- uprobe/core/api.py +364 -0
- uprobe/core/attributes/__init__.py +132 -0
- uprobe/core/attributes/_attributes.py +239 -0
- uprobe/core/cli.py +802 -0
- uprobe/core/gen/__init__.py +0 -0
- uprobe/core/gen/barcodes.py +108 -0
- uprobe/core/gen/fun.py +310 -0
- uprobe/core/gen/probe.py +216 -0
- uprobe/core/gen/utils.py +25 -0
- uprobe/core/process/__init__.py +224 -0
- uprobe/core/process/equal_space.py +48 -0
- uprobe/core/process/otp.py +155 -0
- uprobe/core/process/summary.py +150 -0
- uprobe/core/report/__init__.py +9 -0
- uprobe/core/report/html.py +854 -0
- uprobe/core/report/plot.py +499 -0
- uprobe/core/tools/__init__.py +94 -0
- uprobe/core/tools/aligner.py +85 -0
- uprobe/core/tools/kmerFilter.py +324 -0
- uprobe/core/utils.py +165 -0
- uprobe/http/__init__.py +1 -0
- uprobe/http/__main__.py +4 -0
- uprobe/http/routers/__init__.py +0 -0
- uprobe/http/routers/agent.py +538 -0
- uprobe/http/routers/auth.py +719 -0
- uprobe/http/routers/custom_probes.py +98 -0
- uprobe/http/routers/genome.py +322 -0
- uprobe/http/routers/task.py +548 -0
- uprobe/http/routers/user.py +56 -0
- uprobe/http/routers/workflow.py +284 -0
- uprobe/http/server.py +102 -0
- uprobe/http/utils/__init__.py +0 -0
- uprobe/http/utils/agent_store.py +386 -0
- uprobe/http/utils/file.py +112 -0
- uprobe/http/utils/logger.py +89 -0
- uprobe/http/utils/paths.py +115 -0
- uprobe/http/utils/probe.py +75 -0
- uprobe/http/utils/process_pool.py +25 -0
- uprobe/http/utils/task_queue.py +54 -0
- uprobe/http/utils/uprobe_runner.py +65 -0
- uprobe/http/utils/utils.py +12 -0
- uprobe-0.1.0.dist-info/METADATA +73 -0
- uprobe-0.1.0.dist-info/RECORD +55 -0
- uprobe-0.1.0.dist-info/WHEEL +5 -0
- uprobe-0.1.0.dist-info/entry_points.txt +3 -0
- uprobe-0.1.0.dist-info/licenses/LICENSE +21 -0
- uprobe-0.1.0.dist-info/top_level.txt +1 -0
uprobe/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
U-Probe package.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
__all__ = ["UProbeAPI", "__version__"]
|
|
12
|
+
|
|
13
|
+
def __getattr__(name: str) -> Any:
|
|
14
|
+
"""Lazy attribute access for optional heavy imports."""
|
|
15
|
+
if name == "UProbeAPI":
|
|
16
|
+
from uprobe.core.api import UProbeAPI
|
|
17
|
+
return UProbeAPI
|
|
18
|
+
raise AttributeError(f"module 'uprobe' has no attribute {name!r}")
|
uprobe/__main__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
Main entry point for U-Probe CLI.
|
|
4
|
+
"""
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
# Add project root to sys.path for development
|
|
10
|
+
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
11
|
+
sys.path.append(project_root)
|
|
12
|
+
|
|
13
|
+
from uprobe.core.cli import main
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if __name__ == '__main__':
|
|
17
|
+
main()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Read the Docs configuration file for U-Probe
|
|
2
|
+
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
|
3
|
+
|
|
4
|
+
version: 2
|
|
5
|
+
|
|
6
|
+
# Set the OS, Python version and other tools you might need
|
|
7
|
+
build:
|
|
8
|
+
os: ubuntu-22.04
|
|
9
|
+
tools:
|
|
10
|
+
python: "3.8"
|
|
11
|
+
# You can also specify other tools and their versions:
|
|
12
|
+
# nodejs: "16"
|
|
13
|
+
# rust: "1.55"
|
|
14
|
+
# golang: "1.17"
|
|
15
|
+
|
|
16
|
+
# Build documentation in the docs/ directory with Sphinx
|
|
17
|
+
sphinx:
|
|
18
|
+
configuration: docs/source/conf.py
|
|
19
|
+
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
|
|
20
|
+
# builder: "dirhtml"
|
|
21
|
+
# Fail on all warnings
|
|
22
|
+
fail_on_warning: false
|
|
23
|
+
|
|
24
|
+
# Optionally build your docs in additional formats such as PDF
|
|
25
|
+
formats:
|
|
26
|
+
- pdf
|
|
27
|
+
- epub
|
|
28
|
+
|
|
29
|
+
# Optional but recommended, declare the Python requirements required
|
|
30
|
+
# to build your documentation
|
|
31
|
+
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
|
32
|
+
python:
|
|
33
|
+
install:
|
|
34
|
+
- method: pip
|
|
35
|
+
path: .
|
|
36
|
+
extra_requirements:
|
|
37
|
+
- docs
|
|
38
|
+
- requirements: docs/requirements.txt
|
|
39
|
+
|
|
40
|
+
# Set the version of Python and other tools you might need
|
|
41
|
+
# build:
|
|
42
|
+
# os: ubuntu-22.04
|
|
43
|
+
# tools:
|
|
44
|
+
# python: "3.8"
|
|
45
|
+
# jobs:
|
|
46
|
+
# post_create_environment:
|
|
47
|
+
# # Install poetry
|
|
48
|
+
# - pip install poetry
|
|
49
|
+
# # Tell poetry to not use a virtual environment
|
|
50
|
+
# - poetry config virtualenvs.create false
|
|
51
|
+
# post_install:
|
|
52
|
+
# # Install dependencies
|
|
53
|
+
# - poetry install --with docs
|
uprobe/core/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
U-Probe package.
|
|
3
|
+
|
|
4
|
+
Note:
|
|
5
|
+
This package exposes `UProbeAPI`, but we avoid importing it eagerly to keep
|
|
6
|
+
lightweight entry points (e.g. `python -m uprobe.core.agent.repl_bootstrap`) working
|
|
7
|
+
even in environments where optional runtime dependencies are not installed yet.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from uprobe import __version__
|
|
15
|
+
|
|
16
|
+
__all__ = ["UProbeAPI"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def __getattr__(name: str) -> Any:
|
|
20
|
+
"""Lazy attribute access for optional heavy imports."""
|
|
21
|
+
if name == "UProbeAPI":
|
|
22
|
+
from .api import UProbeAPI # Local import to avoid import-time deps
|
|
23
|
+
|
|
24
|
+
return UProbeAPI
|
|
25
|
+
raise AttributeError(f"module 'uprobe.core' has no attribute {name!r}")
|
|
26
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bootstrap Pantheon REPL in a workspace with the U-Probe team template.
|
|
3
|
+
|
|
4
|
+
This module intentionally bypasses the existing U-Probe CLI and the
|
|
5
|
+
`uprobe.core.agent.api.UProbeAgentAPI` workflow. Instead, it:
|
|
6
|
+
1) Copies `uprobe_team.md` into `<workspace>/.pantheon/teams/`
|
|
7
|
+
2) Launches `python -m pantheon.repl --template <copied_md>`
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _build_arg_parser() -> argparse.ArgumentParser:
|
|
21
|
+
"""Build CLI argument parser."""
|
|
22
|
+
parser = argparse.ArgumentParser(
|
|
23
|
+
prog="python -m uprobe.core.agent.repl_bootstrap",
|
|
24
|
+
description=(
|
|
25
|
+
"Copy uprobe_team.md into <workspace>/.pantheon/teams/ and launch "
|
|
26
|
+
"Pantheon REPL with --template so uprobe_team is loaded by default."
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--workspace",
|
|
31
|
+
type=str,
|
|
32
|
+
default=str(Path.cwd()),
|
|
33
|
+
help="Workspace directory to install templates into (default: CWD).",
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--force",
|
|
37
|
+
action="store_true",
|
|
38
|
+
help="Overwrite existing <workspace>/.pantheon/teams/uprobe_team.md.",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Pass-through options for pantheon.repl (keep it minimal but useful).
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--memory-dir",
|
|
44
|
+
type=str,
|
|
45
|
+
default=None,
|
|
46
|
+
help="Pantheon memory dir (passed to pantheon.repl).",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--log-level",
|
|
50
|
+
type=str,
|
|
51
|
+
default=None,
|
|
52
|
+
help="Log level for REPL (DEBUG, INFO, WARNING, ERROR, CRITICAL).",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--quiet",
|
|
56
|
+
action="store_true",
|
|
57
|
+
help="Disable console logging in REPL (passed to pantheon.repl).",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--resync",
|
|
61
|
+
action="store_true",
|
|
62
|
+
help=(
|
|
63
|
+
"Force pantheon.repl to resync templates "
|
|
64
|
+
"(cleans skills/agents/teams/prompts dirs)."
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--chat-id",
|
|
69
|
+
type=str,
|
|
70
|
+
default=None,
|
|
71
|
+
help="Resume a specific chat ID (passed to pantheon.repl).",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"repl_args",
|
|
75
|
+
nargs=argparse.REMAINDER,
|
|
76
|
+
help=(
|
|
77
|
+
"Additional arguments passed to pantheon.repl after '--'. "
|
|
78
|
+
"Example: -- --log-level DEBUG"
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
return parser
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _normalize_repl_args(remainder: list[str]) -> list[str]:
|
|
85
|
+
"""Normalize argparse REMAINDER (strip leading '--' if present)."""
|
|
86
|
+
if remainder and remainder[0] == "--":
|
|
87
|
+
return remainder[1:]
|
|
88
|
+
return remainder
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _install_team_template(workspace: Path, force: bool) -> Path:
|
|
92
|
+
"""Copy uprobe_team.md into <workspace>/.pantheon/teams/ and return dest."""
|
|
93
|
+
src = Path(__file__).resolve().parent / "templates" / "uprobe_team.md"
|
|
94
|
+
if not src.exists():
|
|
95
|
+
raise FileNotFoundError(f"Team template not found: {src}")
|
|
96
|
+
|
|
97
|
+
dest_dir = workspace / ".pantheon" / "teams"
|
|
98
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
|
|
100
|
+
dest = dest_dir / "uprobe_team.md"
|
|
101
|
+
if dest.exists() and not force:
|
|
102
|
+
return dest
|
|
103
|
+
|
|
104
|
+
shutil.copy2(src, dest)
|
|
105
|
+
return dest
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _install_protocol_template(workspace: Path, force: bool) -> Path:
|
|
109
|
+
"""Copy DEFAULT_PROTOCOL.yaml into <workspace>/ and return dest."""
|
|
110
|
+
src = Path(__file__).resolve().parent / "templates" / "DEFAULT_PROTOCOL.yaml"
|
|
111
|
+
if not src.exists():
|
|
112
|
+
raise FileNotFoundError(f"Protocol template not found: {src}")
|
|
113
|
+
|
|
114
|
+
dest = workspace / "DEFAULT_PROTOCOL.yaml"
|
|
115
|
+
# Skip copying DEFAULT_PROTOCOL.yaml to avoid cluttering the workspace
|
|
116
|
+
# if dest.exists() and not force:
|
|
117
|
+
# return dest
|
|
118
|
+
#
|
|
119
|
+
# shutil.copy2(src, dest)
|
|
120
|
+
return dest
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _launch_repl(
|
|
124
|
+
workspace: Path,
|
|
125
|
+
template_path: Path,
|
|
126
|
+
memory_dir: str | None,
|
|
127
|
+
log_level: str | None,
|
|
128
|
+
quiet: bool,
|
|
129
|
+
resync: bool,
|
|
130
|
+
chat_id: str | None,
|
|
131
|
+
extra_args: list[str],
|
|
132
|
+
) -> int:
|
|
133
|
+
"""Launch pantheon.repl with --template and return exit code."""
|
|
134
|
+
# Ensure `pantheon` can be imported even when running in an arbitrary
|
|
135
|
+
# workspace directory during development (repo not installed site-wide).
|
|
136
|
+
repo_root = Path(__file__).resolve().parents[3]
|
|
137
|
+
env = os.environ.copy()
|
|
138
|
+
pythonpath = env.get("PYTHONPATH", "")
|
|
139
|
+
env["PYTHONPATH"] = (
|
|
140
|
+
f"{repo_root}{os.pathsep}{pythonpath}" if pythonpath else str(repo_root)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
cmd: list[str] = [
|
|
144
|
+
sys.executable,
|
|
145
|
+
"-m",
|
|
146
|
+
"pantheon.repl",
|
|
147
|
+
"--template",
|
|
148
|
+
str(template_path),
|
|
149
|
+
"--workspace",
|
|
150
|
+
str(workspace),
|
|
151
|
+
]
|
|
152
|
+
if memory_dir:
|
|
153
|
+
cmd += ["--memory-dir", memory_dir]
|
|
154
|
+
if chat_id:
|
|
155
|
+
cmd += ["--chat-id", chat_id]
|
|
156
|
+
if log_level:
|
|
157
|
+
cmd += ["--log-level", log_level]
|
|
158
|
+
if quiet:
|
|
159
|
+
cmd += ["--quiet"]
|
|
160
|
+
if resync:
|
|
161
|
+
cmd += ["--resync"]
|
|
162
|
+
if extra_args:
|
|
163
|
+
cmd += extra_args
|
|
164
|
+
|
|
165
|
+
completed = subprocess.run(cmd, cwd=str(workspace), env=env)
|
|
166
|
+
return int(completed.returncode)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def main(argv: list[str] | None = None) -> int:
|
|
170
|
+
"""Main entry point."""
|
|
171
|
+
parser = _build_arg_parser()
|
|
172
|
+
args = parser.parse_args(argv)
|
|
173
|
+
|
|
174
|
+
workspace = Path(args.workspace).expanduser().resolve()
|
|
175
|
+
template_path = _install_team_template(workspace=workspace, force=bool(args.force))
|
|
176
|
+
# _install_protocol_template(workspace=workspace, force=bool(args.force))
|
|
177
|
+
|
|
178
|
+
extra_args = _normalize_repl_args(list(args.repl_args or []))
|
|
179
|
+
return _launch_repl(
|
|
180
|
+
workspace=workspace,
|
|
181
|
+
template_path=template_path,
|
|
182
|
+
memory_dir=args.memory_dir,
|
|
183
|
+
log_level=args.log_level,
|
|
184
|
+
quiet=bool(args.quiet),
|
|
185
|
+
resync=bool(args.resync),
|
|
186
|
+
chat_id=args.chat_id,
|
|
187
|
+
extra_args=extra_args,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
raise SystemExit(main())
|
|
193
|
+
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import uuid
|
|
3
|
+
import shutil
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Any, Optional, List, BinaryIO
|
|
7
|
+
|
|
8
|
+
from pantheon.chatroom import ChatRoom
|
|
9
|
+
from pantheon.factory.template_io import UnifiedMarkdownParser
|
|
10
|
+
from pantheon.utils.vision import parse_image_mentions
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AgentSessionManager:
|
|
16
|
+
"""Manages agent sessions using Pantheon ChatRoom."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, workspace_root: Path | None = None):
|
|
19
|
+
if workspace_root is None:
|
|
20
|
+
workspace_root = Path(__file__).resolve().parents[3]
|
|
21
|
+
self.workspace_root = workspace_root
|
|
22
|
+
# Use absolute path to the template file in the source code
|
|
23
|
+
self.template_path = Path(__file__).resolve().parent / "templates" / "uprobe_team.md"
|
|
24
|
+
self.protocol_template_path = Path(__file__).resolve().parent / "templates" / "DEFAULT_PROTOCOL.yaml"
|
|
25
|
+
self.pantheon_memory_dir = self.workspace_root / ".pantheon" / "memory"
|
|
26
|
+
self.output_dir = Path(os.environ.get("UPROBE_OUTPUT_DIR", str(self.workspace_root / "outputs")))
|
|
27
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
self.chatroom = ChatRoom(memory_dir=str(self.pantheon_memory_dir), workspace_path=str(self.workspace_root))
|
|
29
|
+
self.sessions: Dict[str, Dict[str, Any]] = {}
|
|
30
|
+
|
|
31
|
+
async def initialize(self):
|
|
32
|
+
"""Initialize the ChatRoom and background services."""
|
|
33
|
+
self._ensure_team_template_registered()
|
|
34
|
+
# self._ensure_protocol_template_installed()
|
|
35
|
+
|
|
36
|
+
# Set pantheon log level to DEBUG to print full agent execution output
|
|
37
|
+
from pantheon.utils.log import set_level
|
|
38
|
+
set_level("DEBUG")
|
|
39
|
+
|
|
40
|
+
await self.chatroom.run_setup()
|
|
41
|
+
|
|
42
|
+
def _load_team_template(self, model: Optional[str]) -> Dict[str, Any]:
|
|
43
|
+
"""Load and configure the team template."""
|
|
44
|
+
# Template is already ensured in initialize(), but we resolve the path again to be safe
|
|
45
|
+
team_template_path = self.workspace_root / ".pantheon" / "teams" / "uprobe_team.md"
|
|
46
|
+
if not team_template_path.exists():
|
|
47
|
+
# Fallback if initialize wasn't called or file was deleted
|
|
48
|
+
team_template_path = self._ensure_team_template_registered()
|
|
49
|
+
|
|
50
|
+
parser = UnifiedMarkdownParser()
|
|
51
|
+
team_config = parser.parse_file(team_template_path)
|
|
52
|
+
team_dict = team_config.to_dict()
|
|
53
|
+
team_dict["type"] = "team"
|
|
54
|
+
team_dict["source_path"] = str(team_template_path)
|
|
55
|
+
if model:
|
|
56
|
+
for agent_cfg in team_dict.get("agents", []):
|
|
57
|
+
if isinstance(agent_cfg, dict):
|
|
58
|
+
agent_cfg["model"] = model
|
|
59
|
+
return team_dict
|
|
60
|
+
|
|
61
|
+
def _ensure_team_template_registered(self) -> Path:
|
|
62
|
+
"""Ensure uprobe_team.md is registered under .pantheon/teams."""
|
|
63
|
+
dest_dir = self.workspace_root / ".pantheon" / "teams"
|
|
64
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
dest = dest_dir / "uprobe_team.md"
|
|
66
|
+
if not dest.exists():
|
|
67
|
+
shutil.copy2(self.template_path, dest)
|
|
68
|
+
return dest
|
|
69
|
+
|
|
70
|
+
def _ensure_protocol_template_installed(self) -> Path:
|
|
71
|
+
"""Ensure DEFAULT_PROTOCOL.yaml is present in the workspace root."""
|
|
72
|
+
dest = self.workspace_root / "DEFAULT_PROTOCOL.yaml"
|
|
73
|
+
# Skip copying DEFAULT_PROTOCOL.yaml to avoid cluttering the workspace
|
|
74
|
+
# if not dest.exists() and self.protocol_template_path.exists():
|
|
75
|
+
# shutil.copy2(self.protocol_template_path, dest)
|
|
76
|
+
# logger.info(f"Installed protocol template to {dest}")
|
|
77
|
+
return dest
|
|
78
|
+
|
|
79
|
+
def _build_chat_message(self, content: str) -> List[dict]:
|
|
80
|
+
"""Build chat message with image support."""
|
|
81
|
+
return parse_image_mentions(content)
|
|
82
|
+
|
|
83
|
+
async def upload_file(self, session_id: str, file_obj: BinaryIO, filename: str, upload_dir: Optional[Path] = None) -> Dict[str, Any]:
|
|
84
|
+
"""Save an uploaded file and return its metadata."""
|
|
85
|
+
if upload_dir is None:
|
|
86
|
+
upload_dir = self.output_dir / "uploads" / session_id
|
|
87
|
+
upload_dir.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
file_id = str(uuid.uuid4())
|
|
89
|
+
safe_filename = Path(filename).name
|
|
90
|
+
file_path = upload_dir / safe_filename
|
|
91
|
+
with open(file_path, "wb") as f:
|
|
92
|
+
shutil.copyfileobj(file_obj, f)
|
|
93
|
+
size = file_path.stat().st_size
|
|
94
|
+
logger.info(f"Saved uploaded file to {file_path}")
|
|
95
|
+
if session_id in self.sessions:
|
|
96
|
+
if "uploads" not in self.sessions[session_id]:
|
|
97
|
+
self.sessions[session_id]["uploads"] = {}
|
|
98
|
+
self.sessions[session_id]["uploads"][file_id] = {"path": str(file_path), "filename": safe_filename, "size": size}
|
|
99
|
+
return {"id": file_id, "filename": safe_filename, "path": str(file_path), "size": size}
|
|
100
|
+
|
|
101
|
+
async def create_session(self, model: str = "gpt-4", api_key: Optional[str] = None, api_base: Optional[str] = None, proxy: Optional[str] = None) -> str:
|
|
102
|
+
"""Start a new agent session."""
|
|
103
|
+
if api_key:
|
|
104
|
+
os.environ["OPENAI_API_KEY"] = api_key
|
|
105
|
+
if api_base:
|
|
106
|
+
os.environ["OPENAI_API_BASE"] = api_base
|
|
107
|
+
if proxy:
|
|
108
|
+
os.environ["http_proxy"] = proxy
|
|
109
|
+
os.environ["https_proxy"] = proxy
|
|
110
|
+
await self.initialize()
|
|
111
|
+
create_res = await self.chatroom.create_chat("http-session")
|
|
112
|
+
if not create_res.get("success"):
|
|
113
|
+
raise RuntimeError(f"Failed to create chat: {create_res.get('message')}")
|
|
114
|
+
chat_id = create_res["chat_id"]
|
|
115
|
+
try:
|
|
116
|
+
template = self._load_team_template(model)
|
|
117
|
+
setup_res = await self.chatroom.setup_team_for_chat(chat_id, template)
|
|
118
|
+
if not setup_res.get("success"):
|
|
119
|
+
raise RuntimeError(f"Failed to setup team: {setup_res.get('message')}")
|
|
120
|
+
except Exception as e:
|
|
121
|
+
await self.chatroom.delete_chat(chat_id)
|
|
122
|
+
raise e
|
|
123
|
+
self.sessions[chat_id] = {"chat_id": chat_id, "model": model}
|
|
124
|
+
logger.info(f"Started agent session {chat_id} with model={model}")
|
|
125
|
+
return chat_id
|
|
126
|
+
|
|
127
|
+
async def chat(self, session_id: str, content: str, attachment_ids: Optional[List[str]] = None, process_step_message=None) -> Dict[str, Any]:
|
|
128
|
+
"""Send a message to an active session."""
|
|
129
|
+
if session_id not in self.sessions:
|
|
130
|
+
pass
|
|
131
|
+
if attachment_ids is None:
|
|
132
|
+
attachment_ids = []
|
|
133
|
+
if attachment_ids and session_id in self.sessions:
|
|
134
|
+
uploads = self.sessions[session_id].get("uploads", {})
|
|
135
|
+
attached_files = []
|
|
136
|
+
for aid in attachment_ids:
|
|
137
|
+
if aid in uploads:
|
|
138
|
+
attached_files.append(uploads[aid])
|
|
139
|
+
if attached_files:
|
|
140
|
+
file_context = "\n\nUser uploaded the following files:\n"
|
|
141
|
+
for f in attached_files:
|
|
142
|
+
file_context += f"- {f['filename']} (Path: {f['path']})\n"
|
|
143
|
+
content += file_context
|
|
144
|
+
chat_message = self._build_chat_message(content)
|
|
145
|
+
response = await self.chatroom.chat(session_id, chat_message, process_step_message=process_step_message)
|
|
146
|
+
return response
|
|
147
|
+
|
|
148
|
+
async def rewind_and_rerun(self, session_id: str, user_turn_index: int, content: str, attachment_ids: Optional[List[str]] = None, process_step_message=None) -> Dict[str, Any]:
|
|
149
|
+
"""Rewind chat history to a user turn and rerun from edited content."""
|
|
150
|
+
if session_id not in self.sessions:
|
|
151
|
+
pass
|
|
152
|
+
if attachment_ids is None:
|
|
153
|
+
attachment_ids = []
|
|
154
|
+
try:
|
|
155
|
+
await self.chatroom.stop_chat(session_id)
|
|
156
|
+
except Exception:
|
|
157
|
+
pass
|
|
158
|
+
memory = self.chatroom.memory_manager.get_memory(session_id)
|
|
159
|
+
user_turns = memory.get_user_turns()
|
|
160
|
+
if user_turn_index < 0 or user_turn_index >= len(user_turns):
|
|
161
|
+
raise ValueError("Invalid user_turn_index")
|
|
162
|
+
target_index = user_turns[user_turn_index][0]
|
|
163
|
+
memory.revert_to_message(target_index)
|
|
164
|
+
try:
|
|
165
|
+
await memory.flush()
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
return await self.chat(session_id, content, attachment_ids, process_step_message=process_step_message)
|
|
169
|
+
|
|
170
|
+
async def delete_upload(self, session_id: str, attachment_id: str) -> bool:
|
|
171
|
+
"""Delete an uploaded file by attachment id."""
|
|
172
|
+
uploads = self.sessions.get(session_id, {}).get("uploads", {})
|
|
173
|
+
meta = uploads.pop(attachment_id, None)
|
|
174
|
+
if not meta:
|
|
175
|
+
return False
|
|
176
|
+
file_path = Path(meta.get("path", ""))
|
|
177
|
+
if file_path.exists():
|
|
178
|
+
file_path.unlink()
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
async def stop_session(self, session_id: str) -> bool:
|
|
182
|
+
"""Stop and clean up a session."""
|
|
183
|
+
if session_id in self.sessions:
|
|
184
|
+
del self.sessions[session_id]
|
|
185
|
+
try:
|
|
186
|
+
await self.chatroom.stop_chat(session_id)
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
result = await self.chatroom.delete_chat(session_id)
|
|
190
|
+
return result.get("success", False)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
_INSTANCE: Optional[AgentSessionManager] = None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def get_session_manager(workspace_root: Path | None = None, output_dir: Path | None = None, memory_dir: Path | None = None) -> AgentSessionManager:
|
|
197
|
+
"""Get or create the global session manager instance."""
|
|
198
|
+
global _INSTANCE
|
|
199
|
+
if _INSTANCE is None:
|
|
200
|
+
_INSTANCE = AgentSessionManager(workspace_root=workspace_root)
|
|
201
|
+
if output_dir:
|
|
202
|
+
_INSTANCE.output_dir = output_dir
|
|
203
|
+
_INSTANCE.output_dir.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
if memory_dir:
|
|
205
|
+
_INSTANCE.pantheon_memory_dir = memory_dir
|
|
206
|
+
_INSTANCE.chatroom = ChatRoom(memory_dir=str(_INSTANCE.pantheon_memory_dir), workspace_path=str(_INSTANCE.workspace_root))
|
|
207
|
+
return _INSTANCE
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: # protocol name
|
|
2
|
+
|
|
3
|
+
description: Protocol for designing probe type xxx from species xxx
|
|
4
|
+
|
|
5
|
+
genome: # genome name
|
|
6
|
+
|
|
7
|
+
extracts:
|
|
8
|
+
target_region:
|
|
9
|
+
source: # genome | exon | CDS | UTR
|
|
10
|
+
length: # int
|
|
11
|
+
overlap: # int
|
|
12
|
+
|
|
13
|
+
targets:
|
|
14
|
+
- xxx1: # chr:start-end | gene_name | name: string
|
|
15
|
+
- xxx2: # chr:start-end | gene_name | name: string
|
|
16
|
+
|
|
17
|
+
encoding:
|
|
18
|
+
xxx1:
|
|
19
|
+
BC1: xxx
|
|
20
|
+
BC2: xxx
|
|
21
|
+
xxx2:
|
|
22
|
+
BC1: xxx
|
|
23
|
+
BC2: xxx
|
|
24
|
+
|
|
25
|
+
probes:
|
|
26
|
+
|
|
27
|
+
# NOTE:
|
|
28
|
+
# - `attributes` and `post_process` can be omitted.
|
|
29
|
+
# - If they are missing or empty, the CLI will auto-generate defaults
|
|
30
|
+
# based on DNA (source=genome) vs RNA (source=exon/CDS/UTR) mode.
|
|
31
|
+
attributes:
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
post_process:
|
|
35
|
+
filters:
|
|
36
|
+
|
|
37
|
+
# clean
|
|
38
|
+
avoid_otp:
|
|
39
|
+
|
|
40
|
+
# equal_space
|
|
41
|
+
equal_space:
|
|
42
|
+
|
|
43
|
+
# remove overlap
|
|
44
|
+
remove_overlap:
|
|
45
|
+
location_interval: 0
|
|
46
|
+
|
|
47
|
+
sorts:
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
summary:
|
|
52
|
+
report_name: dna_report # template name # class: dna_report or rna_report
|
|
53
|
+
attributes:
|
|
54
|
+
|
|
55
|
+
|