autobots-devtools-shared-lib 0.2.1__tar.gz → 0.2.3a2__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.
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/PKG-INFO +1 -1
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/pyproject.toml +1 -1
- autobots_devtools_shared_lib-0.2.3a2/src/autobots_devtools_shared_lib/common/jenkins/__init__.py +26 -0
- autobots_devtools_shared_lib-0.2.3a2/src/autobots_devtools_shared_lib/common/jenkins/builtin_tools.py +89 -0
- autobots_devtools_shared_lib-0.2.3a2/src/autobots_devtools_shared_lib/common/jenkins/config.py +105 -0
- autobots_devtools_shared_lib-0.2.3a2/src/autobots_devtools_shared_lib/common/jenkins/http_utils.py +147 -0
- autobots_devtools_shared_lib-0.2.3a2/src/autobots_devtools_shared_lib/common/jenkins/loader.py +47 -0
- autobots_devtools_shared_lib-0.2.3a2/src/autobots_devtools_shared_lib/common/jenkins/tools.py +167 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/servers/fileserver/app.py +6 -1
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/__init__.py +7 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/config/dynagent_settings.py +0 -5
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/tools/state_tools.py +4 -25
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/tools/tool_registry.py +40 -2
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/ui/default_ui.py +1 -1
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/README.md +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/__init__.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/__init__.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/observability/__init__.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/observability/logging_utils.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/observability/otel_fastapi.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/observability/trace_metadata.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/observability/trace_propagation.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/observability/tracing.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/servers/__init__.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/servers/fileserver/README.md +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/servers/fileserver/__init__.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/servers/fileserver/config.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/servers/fileserver/models.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/services/__init__.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/services/context/README.md +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/services/context/__init__.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/services/context/cache_backed.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/services/context/db_repository.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/services/context/factory.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/services/context/in_memory.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/services/context/redis_store.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/services/context/store.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/tools/__init__.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/tools/context_tools.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/tools/format_tools.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/tools/fserver_client_tools.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/utils/__init__.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/utils/context_utils.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/utils/format_utils.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/utils/fserver_client_utils.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/agents/__init__.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/agents/agent_config_utils.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/agents/agent_meta.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/agents/base_agent.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/agents/batch.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/agents/invocation_utils.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/agents/middleware.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/config/__init__.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/llm/__init__.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/llm/llm.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/models/__init__.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/models/state.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/services/__init__.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/services/structured_converter.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/tools/__init__.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/ui/__init__.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/ui/ui_utils.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/utils/__init__.py +0 -0
- {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/py.typed +0 -0
autobots_devtools_shared_lib-0.2.3a2/src/autobots_devtools_shared_lib/common/jenkins/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# ABOUTME: Jenkins pipeline trigger package — config, loader, and tool generation.
|
|
2
|
+
# ABOUTME: Part of the common layer; usable by any dynagent-based application.
|
|
3
|
+
|
|
4
|
+
from autobots_devtools_shared_lib.common.jenkins.config import (
|
|
5
|
+
JenkinsAuthConfig,
|
|
6
|
+
JenkinsConfig,
|
|
7
|
+
JenkinsParameterConfig,
|
|
8
|
+
JenkinsPipelineConfig,
|
|
9
|
+
JenkinsPollingConfig,
|
|
10
|
+
)
|
|
11
|
+
from autobots_devtools_shared_lib.common.jenkins.loader import load_jenkins_config
|
|
12
|
+
from autobots_devtools_shared_lib.common.jenkins.tools import (
|
|
13
|
+
create_jenkins_tools,
|
|
14
|
+
register_pipeline_tools,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"JenkinsAuthConfig",
|
|
19
|
+
"JenkinsConfig",
|
|
20
|
+
"JenkinsParameterConfig",
|
|
21
|
+
"JenkinsPipelineConfig",
|
|
22
|
+
"JenkinsPollingConfig",
|
|
23
|
+
"create_jenkins_tools",
|
|
24
|
+
"load_jenkins_config",
|
|
25
|
+
"register_pipeline_tools",
|
|
26
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# ABOUTME: Built-in generic Jenkins observability tools.
|
|
2
|
+
# ABOUTME: Provides get_jenkins_build_status and get_jenkins_console_log.
|
|
3
|
+
# ABOUTME: Call set_jenkins_config() once at startup before these tools are invoked.
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
from langchain.tools import tool
|
|
11
|
+
|
|
12
|
+
from autobots_devtools_shared_lib.common.jenkins.http_utils import get_auth
|
|
13
|
+
from autobots_devtools_shared_lib.common.observability.logging_utils import get_logger
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from autobots_devtools_shared_lib.common.jenkins.config import JenkinsConfig
|
|
17
|
+
|
|
18
|
+
logger = get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
_config: JenkinsConfig | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def set_jenkins_config(config: JenkinsConfig) -> None:
|
|
24
|
+
"""Store the Jenkins config for use by the builtin tools at call time."""
|
|
25
|
+
global _config
|
|
26
|
+
_config = config
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_config() -> JenkinsConfig:
|
|
30
|
+
if _config is None:
|
|
31
|
+
raise RuntimeError("Jenkins builtin tools used before set_jenkins_config() was called")
|
|
32
|
+
return _config
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@tool
|
|
36
|
+
def get_jenkins_build_status(job_name: str, build_number: int | None = None) -> str:
|
|
37
|
+
"""Get the current status of a Jenkins build.
|
|
38
|
+
|
|
39
|
+
Returns a concise string with job name, build number,
|
|
40
|
+
status (SUCCESS / FAILURE / IN_PROGRESS), and build URL.
|
|
41
|
+
Omit build_number to check the latest build.
|
|
42
|
+
"""
|
|
43
|
+
config = _get_config()
|
|
44
|
+
base_url = config.base_url.rstrip("/")
|
|
45
|
+
auth = get_auth(config)
|
|
46
|
+
if build_number is None:
|
|
47
|
+
api_url = f"{base_url}/job/{job_name}/lastBuild/api/json"
|
|
48
|
+
else:
|
|
49
|
+
api_url = f"{base_url}/job/{job_name}/{build_number}/api/json"
|
|
50
|
+
try:
|
|
51
|
+
resp = requests.get(api_url, auth=auth, timeout=30)
|
|
52
|
+
resp.raise_for_status()
|
|
53
|
+
data = resp.json()
|
|
54
|
+
number = data.get("number")
|
|
55
|
+
building = data.get("building", False)
|
|
56
|
+
result = data.get("result")
|
|
57
|
+
url = data.get("url", "")
|
|
58
|
+
status = "IN_PROGRESS" if (building or result is None) else str(result).upper()
|
|
59
|
+
except requests.RequestException as exc:
|
|
60
|
+
logger.exception(f"Error getting build status for '{job_name}'")
|
|
61
|
+
return f"Error getting build status: {exc}"
|
|
62
|
+
else:
|
|
63
|
+
return f"job={job_name} build={number} status={status} url={url}"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@tool
|
|
67
|
+
def get_jenkins_console_log(job_name: str, build_number: int | None = None, start: int = 0) -> str:
|
|
68
|
+
"""Retrieve the console log output for a Jenkins build.
|
|
69
|
+
|
|
70
|
+
Returns the log text from the given byte offset.
|
|
71
|
+
Use start=0 (default) for the full log.
|
|
72
|
+
Omit build_number to use the latest build.
|
|
73
|
+
Useful for diagnosing pipeline failures.
|
|
74
|
+
"""
|
|
75
|
+
config = _get_config()
|
|
76
|
+
base_url = config.base_url.rstrip("/")
|
|
77
|
+
auth = get_auth(config)
|
|
78
|
+
if build_number is None:
|
|
79
|
+
log_url = f"{base_url}/job/{job_name}/lastBuild/logText/progressiveText?start={start}"
|
|
80
|
+
else:
|
|
81
|
+
log_url = f"{base_url}/job/{job_name}/{build_number}/logText/progressiveText?start={start}"
|
|
82
|
+
try:
|
|
83
|
+
resp = requests.get(log_url, auth=auth, timeout=30)
|
|
84
|
+
resp.raise_for_status()
|
|
85
|
+
except requests.RequestException as exc:
|
|
86
|
+
logger.exception(f"Error getting console log for '{job_name}'")
|
|
87
|
+
return f"Error getting console log: {exc}"
|
|
88
|
+
else:
|
|
89
|
+
return resp.text
|
autobots_devtools_shared_lib-0.2.3a2/src/autobots_devtools_shared_lib/common/jenkins/config.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# ABOUTME: Pydantic models for jenkins.yaml configuration.
|
|
2
|
+
# ABOUTME: Defines auth, polling, parameter, tool, and top-level Jenkins config models.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class JenkinsAuthConfig(BaseModel):
|
|
10
|
+
"""Credentials resolved from environment variables at runtime."""
|
|
11
|
+
|
|
12
|
+
username_env: str = Field(
|
|
13
|
+
default="JENKINS_USERNAME",
|
|
14
|
+
description="Name of the env var holding the Jenkins username",
|
|
15
|
+
)
|
|
16
|
+
token_env: str = Field(
|
|
17
|
+
default="JENKINS_API_TOKEN",
|
|
18
|
+
description="Name of the env var holding the Jenkins API token",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class JenkinsPollingConfig(BaseModel):
|
|
23
|
+
"""Controls whether and how long to wait for a triggered build to complete."""
|
|
24
|
+
|
|
25
|
+
wait_for_completion: bool = Field(
|
|
26
|
+
default=True,
|
|
27
|
+
description="Whether to block until the build finishes",
|
|
28
|
+
)
|
|
29
|
+
poll_interval_seconds: int = Field(
|
|
30
|
+
default=10,
|
|
31
|
+
description="Seconds to wait between build-status polls",
|
|
32
|
+
)
|
|
33
|
+
max_wait_seconds: int = Field(
|
|
34
|
+
default=300,
|
|
35
|
+
description="Maximum seconds to wait before giving up",
|
|
36
|
+
)
|
|
37
|
+
queue_max_retries: int = Field(
|
|
38
|
+
default=5,
|
|
39
|
+
description="Max attempts to resolve a build number from the Jenkins queue",
|
|
40
|
+
)
|
|
41
|
+
queue_retry_delay_seconds: int = Field(
|
|
42
|
+
default=2,
|
|
43
|
+
description="Seconds to wait between queue-item polls",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class JenkinsParameterConfig(BaseModel):
|
|
48
|
+
"""A single parameter that will be forwarded as a query string to Jenkins."""
|
|
49
|
+
|
|
50
|
+
type: str = Field(
|
|
51
|
+
default="string",
|
|
52
|
+
description="Parameter type: 'string', 'boolean', or 'integer'",
|
|
53
|
+
)
|
|
54
|
+
description: str = Field(
|
|
55
|
+
default="",
|
|
56
|
+
description="LLM-facing hint describing what value to provide",
|
|
57
|
+
)
|
|
58
|
+
required: bool = Field(
|
|
59
|
+
default=True,
|
|
60
|
+
description="Whether the LLM must always supply this parameter",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class JenkinsPipelineConfig(BaseModel):
|
|
65
|
+
"""Configuration for a single Jenkins pipeline entry.
|
|
66
|
+
|
|
67
|
+
The framework registers this as a LangChain tool named ``{key}_tool``,
|
|
68
|
+
so pipeline keys in jenkins.yaml should not carry a ``_tool`` suffix.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
uri: str = Field(
|
|
72
|
+
description="Relative URI path for the Jenkins pipeline, e.g. /job/my-job/buildWithParameters"
|
|
73
|
+
)
|
|
74
|
+
description: str = Field(
|
|
75
|
+
default="",
|
|
76
|
+
description="LLM-facing docstring describing what this pipeline does",
|
|
77
|
+
)
|
|
78
|
+
parameters: dict[str, JenkinsParameterConfig] = Field(
|
|
79
|
+
default_factory=dict,
|
|
80
|
+
description="Parameters forwarded as query strings to Jenkins",
|
|
81
|
+
)
|
|
82
|
+
polling: JenkinsPollingConfig | None = Field(
|
|
83
|
+
default=None,
|
|
84
|
+
description="Per-pipeline polling overrides; falls back to global polling config when None",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class JenkinsConfig(BaseModel):
|
|
89
|
+
"""Top-level Jenkins configuration loaded from jenkins.yaml."""
|
|
90
|
+
|
|
91
|
+
base_url: str = Field(description="Jenkins base URL, e.g. https://jenkins.example.com")
|
|
92
|
+
auth: JenkinsAuthConfig = Field(
|
|
93
|
+
default_factory=JenkinsAuthConfig,
|
|
94
|
+
description="Authentication configuration (env var names for credentials)",
|
|
95
|
+
)
|
|
96
|
+
polling: JenkinsPollingConfig = Field(
|
|
97
|
+
default_factory=JenkinsPollingConfig,
|
|
98
|
+
description="Global polling defaults, overridable per pipeline",
|
|
99
|
+
)
|
|
100
|
+
pipelines: dict[str, JenkinsPipelineConfig] = Field(
|
|
101
|
+
description=(
|
|
102
|
+
"Named pipeline definitions. Each key is the pipeline identifier; "
|
|
103
|
+
"the framework registers it as a LangChain tool named ``{key}_tool``."
|
|
104
|
+
)
|
|
105
|
+
)
|
autobots_devtools_shared_lib-0.2.3a2/src/autobots_devtools_shared_lib/common/jenkins/http_utils.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# ABOUTME: Low-level HTTP helpers for Jenkins API interactions.
|
|
2
|
+
# ABOUTME: Handles auth resolution, queue polling, build completion waiting, and URL parsing.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from autobots_devtools_shared_lib.common.observability.logging_utils import get_logger
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from autobots_devtools_shared_lib.common.jenkins.config import (
|
|
16
|
+
JenkinsConfig,
|
|
17
|
+
JenkinsPollingConfig,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_auth(config: JenkinsConfig) -> tuple[str, str] | None:
|
|
24
|
+
"""Resolve Basic Auth credentials from environment variables.
|
|
25
|
+
|
|
26
|
+
Returns a (username, token) tuple, or None if either env var is unset.
|
|
27
|
+
"""
|
|
28
|
+
username = os.getenv(config.auth.username_env, "")
|
|
29
|
+
token = os.getenv(config.auth.token_env, "")
|
|
30
|
+
if username and token:
|
|
31
|
+
return (username, token)
|
|
32
|
+
logger.warning(
|
|
33
|
+
f"Jenkins auth env vars '{config.auth.username_env}' / '{config.auth.token_env}' "
|
|
34
|
+
"not set — requests will be unauthenticated"
|
|
35
|
+
)
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def poll_queue_for_build_number(
|
|
40
|
+
queue_location: str,
|
|
41
|
+
polling: JenkinsPollingConfig,
|
|
42
|
+
auth: tuple[str, str] | None,
|
|
43
|
+
) -> dict[str, Any]:
|
|
44
|
+
"""Poll the Jenkins queue API until a build number is assigned.
|
|
45
|
+
|
|
46
|
+
Returns a dict with keys: status ('success' | 'queued' | 'error'),
|
|
47
|
+
message, build_number, build_url.
|
|
48
|
+
"""
|
|
49
|
+
if not queue_location:
|
|
50
|
+
msg = "No queue location returned — build may not have triggered"
|
|
51
|
+
logger.error(msg)
|
|
52
|
+
return {"status": "error", "message": msg, "build_number": None, "build_url": None}
|
|
53
|
+
|
|
54
|
+
time.sleep(1)
|
|
55
|
+
queue_api_url = f"{queue_location}api/json"
|
|
56
|
+
logger.info(f"Polling Jenkins queue: {queue_api_url}")
|
|
57
|
+
|
|
58
|
+
for attempt in range(polling.queue_max_retries):
|
|
59
|
+
try:
|
|
60
|
+
resp = requests.get(queue_api_url, auth=auth, timeout=30)
|
|
61
|
+
resp.raise_for_status()
|
|
62
|
+
data = resp.json()
|
|
63
|
+
executable = data.get("executable")
|
|
64
|
+
if executable:
|
|
65
|
+
build_number = executable.get("number")
|
|
66
|
+
build_url = executable.get("url")
|
|
67
|
+
logger.info(f"Build #{build_number} assigned: {build_url}")
|
|
68
|
+
return {
|
|
69
|
+
"status": "success",
|
|
70
|
+
"message": f"Build #{build_number} assigned",
|
|
71
|
+
"build_number": build_number,
|
|
72
|
+
"build_url": build_url,
|
|
73
|
+
}
|
|
74
|
+
if attempt < polling.queue_max_retries - 1:
|
|
75
|
+
logger.debug(
|
|
76
|
+
f"Queue attempt {attempt + 1}/{polling.queue_max_retries}: "
|
|
77
|
+
f"build not yet assigned, waiting {polling.queue_retry_delay_seconds}s"
|
|
78
|
+
)
|
|
79
|
+
time.sleep(polling.queue_retry_delay_seconds)
|
|
80
|
+
except requests.RequestException as exc:
|
|
81
|
+
logger.exception(f"Queue poll error (attempt {attempt + 1})")
|
|
82
|
+
if attempt < polling.queue_max_retries - 1:
|
|
83
|
+
time.sleep(polling.queue_retry_delay_seconds)
|
|
84
|
+
else:
|
|
85
|
+
return {
|
|
86
|
+
"status": "error",
|
|
87
|
+
"message": f"Queue poll failed after {polling.queue_max_retries} attempts: {exc}",
|
|
88
|
+
"build_number": None,
|
|
89
|
+
"build_url": None,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"status": "queued",
|
|
94
|
+
"message": f"Build queued but not yet assigned after {polling.queue_max_retries} attempts",
|
|
95
|
+
"build_number": None,
|
|
96
|
+
"build_url": None,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def wait_for_build(
|
|
101
|
+
base_url: str,
|
|
102
|
+
job_name: str,
|
|
103
|
+
build_number: int,
|
|
104
|
+
polling: JenkinsPollingConfig,
|
|
105
|
+
auth: tuple[str, str] | None,
|
|
106
|
+
) -> str:
|
|
107
|
+
"""Poll the build API until the result is non-null or the timeout is reached."""
|
|
108
|
+
elapsed = 0
|
|
109
|
+
while elapsed < polling.max_wait_seconds:
|
|
110
|
+
api_url = f"{base_url}/job/{job_name}/{build_number}/api/json"
|
|
111
|
+
try:
|
|
112
|
+
resp = requests.get(api_url, auth=auth, timeout=30)
|
|
113
|
+
resp.raise_for_status()
|
|
114
|
+
data = resp.json()
|
|
115
|
+
building = data.get("building", False)
|
|
116
|
+
result = data.get("result")
|
|
117
|
+
build_url = data.get("url", "")
|
|
118
|
+
if not building and result is not None:
|
|
119
|
+
logger.info(f"Build #{build_number} completed: {result}")
|
|
120
|
+
return f"job={job_name} build={build_number} result={result} url={build_url}"
|
|
121
|
+
logger.debug(
|
|
122
|
+
f"Build #{build_number} in progress; "
|
|
123
|
+
f"waiting {polling.poll_interval_seconds}s (elapsed={elapsed}s)"
|
|
124
|
+
)
|
|
125
|
+
time.sleep(polling.poll_interval_seconds)
|
|
126
|
+
elapsed += polling.poll_interval_seconds
|
|
127
|
+
except requests.RequestException as exc:
|
|
128
|
+
logger.exception(f"Build poll error for {job_name}#{build_number}")
|
|
129
|
+
return f"Error polling build status: {exc}"
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
f"Timeout: build #{build_number} for job '{job_name}' "
|
|
133
|
+
f"did not complete within {polling.max_wait_seconds}s"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def extract_job_name_from_url(url: str) -> str:
|
|
138
|
+
"""Extract the Jenkins job name from a relative URL.
|
|
139
|
+
|
|
140
|
+
Example: /job/create-workspace/buildWithParameters → 'create-workspace'
|
|
141
|
+
"""
|
|
142
|
+
parts = [p for p in url.split("/") if p]
|
|
143
|
+
try:
|
|
144
|
+
job_idx = parts.index("job")
|
|
145
|
+
return parts[job_idx + 1]
|
|
146
|
+
except (ValueError, IndexError):
|
|
147
|
+
return url
|
autobots_devtools_shared_lib-0.2.3a2/src/autobots_devtools_shared_lib/common/jenkins/loader.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# ABOUTME: Loads jenkins.yaml from the same config directory as agents.yaml.
|
|
2
|
+
# ABOUTME: Returns None silently when jenkins.yaml is absent (feature is opt-in).
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from autobots_devtools_shared_lib.common.jenkins.config import JenkinsConfig
|
|
9
|
+
from autobots_devtools_shared_lib.common.observability.logging_utils import get_logger
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_jenkins_config() -> JenkinsConfig | None:
|
|
15
|
+
"""Load and validate jenkins.yaml from the active config directory.
|
|
16
|
+
|
|
17
|
+
Reads from the same directory as agents.yaml (controlled by
|
|
18
|
+
DYNAGENT_CONFIG_ROOT_DIR). Returns None if jenkins.yaml is absent so
|
|
19
|
+
callers do not need to handle the optional file specially.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Validated JenkinsConfig or None if the file does not exist.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
yaml.YAMLError: If the file contains invalid YAML.
|
|
26
|
+
pydantic.ValidationError: If the YAML structure does not match the schema.
|
|
27
|
+
"""
|
|
28
|
+
from autobots_devtools_shared_lib.dynagent.agents.agent_config_utils import get_config_dir
|
|
29
|
+
|
|
30
|
+
config_dir = get_config_dir()
|
|
31
|
+
jenkins_path = config_dir / "jenkins.yaml"
|
|
32
|
+
|
|
33
|
+
if not jenkins_path.exists():
|
|
34
|
+
logger.debug(
|
|
35
|
+
f"No jenkins.yaml found at {jenkins_path}; Jenkins tools will not be registered"
|
|
36
|
+
)
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
logger.info(f"Loading Jenkins config from {jenkins_path}")
|
|
40
|
+
with open(jenkins_path) as f: # noqa: PTH123
|
|
41
|
+
raw = yaml.safe_load(f)
|
|
42
|
+
|
|
43
|
+
config = JenkinsConfig.model_validate(raw["jenkins_config"])
|
|
44
|
+
logger.info(
|
|
45
|
+
f"Loaded Jenkins config with {len(config.pipelines)} pipeline(s): {list(config.pipelines)}"
|
|
46
|
+
)
|
|
47
|
+
return config
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# ABOUTME: Generates LangChain StructuredTools from JenkinsConfig at startup.
|
|
2
|
+
# ABOUTME: One tool is created per pipeline entry; tool name = "{pipeline_key}_tool".
|
|
3
|
+
# ABOUTME: register_pipeline_tools() is the single entry point for auto-registration.
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
from langchain_core.tools import StructuredTool
|
|
11
|
+
from pydantic import BaseModel, Field, create_model
|
|
12
|
+
|
|
13
|
+
from autobots_devtools_shared_lib.common.jenkins.builtin_tools import (
|
|
14
|
+
get_jenkins_build_status,
|
|
15
|
+
get_jenkins_console_log,
|
|
16
|
+
set_jenkins_config,
|
|
17
|
+
)
|
|
18
|
+
from autobots_devtools_shared_lib.common.jenkins.http_utils import (
|
|
19
|
+
extract_job_name_from_url,
|
|
20
|
+
get_auth,
|
|
21
|
+
poll_queue_for_build_number,
|
|
22
|
+
wait_for_build,
|
|
23
|
+
)
|
|
24
|
+
from autobots_devtools_shared_lib.common.observability.logging_utils import get_logger
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from autobots_devtools_shared_lib.common.jenkins.config import (
|
|
28
|
+
JenkinsConfig,
|
|
29
|
+
JenkinsPipelineConfig,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
_PYTHON_TYPE_MAP: dict[str, type] = {
|
|
35
|
+
"string": str,
|
|
36
|
+
"str": str,
|
|
37
|
+
"boolean": bool,
|
|
38
|
+
"bool": bool,
|
|
39
|
+
"integer": int,
|
|
40
|
+
"int": int,
|
|
41
|
+
"float": float,
|
|
42
|
+
"number": float,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _build_args_schema(tool_name: str, pipeline_cfg: JenkinsPipelineConfig) -> type[BaseModel]:
|
|
47
|
+
"""Build a dynamic Pydantic model from a pipeline's parameters.
|
|
48
|
+
|
|
49
|
+
Required parameters get a plain Field; optional ones get ``type | None`` with default None.
|
|
50
|
+
"""
|
|
51
|
+
field_definitions: dict[str, Any] = {}
|
|
52
|
+
for param_name, param_cfg in pipeline_cfg.parameters.items():
|
|
53
|
+
py_type = _PYTHON_TYPE_MAP.get(param_cfg.type.lower(), str)
|
|
54
|
+
if param_cfg.required:
|
|
55
|
+
field_definitions[param_name] = (py_type, Field(description=param_cfg.description))
|
|
56
|
+
else:
|
|
57
|
+
field_definitions[param_name] = (
|
|
58
|
+
py_type | None,
|
|
59
|
+
Field(default=None, description=param_cfg.description),
|
|
60
|
+
)
|
|
61
|
+
return create_model(f"{tool_name}_args", **field_definitions)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _make_trigger_fn(
|
|
65
|
+
tool_name: str,
|
|
66
|
+
pipeline_cfg: JenkinsPipelineConfig,
|
|
67
|
+
config: JenkinsConfig,
|
|
68
|
+
) -> Any:
|
|
69
|
+
"""Return a closure that triggers the Jenkins pipeline and handles completion polling."""
|
|
70
|
+
effective_polling = pipeline_cfg.polling or config.polling
|
|
71
|
+
full_url = config.base_url.rstrip("/") + pipeline_cfg.uri
|
|
72
|
+
|
|
73
|
+
def trigger(**kwargs: Any) -> str:
|
|
74
|
+
auth = get_auth(config)
|
|
75
|
+
query_params = {k: v for k, v in kwargs.items() if v is not None}
|
|
76
|
+
logger.info(
|
|
77
|
+
f"Triggering Jenkins tool '{tool_name}': POST {full_url} params={list(query_params)}"
|
|
78
|
+
)
|
|
79
|
+
try:
|
|
80
|
+
response = requests.post(full_url, params=query_params, auth=auth, timeout=30)
|
|
81
|
+
response.raise_for_status()
|
|
82
|
+
except requests.RequestException as exc:
|
|
83
|
+
logger.exception(f"Jenkins trigger failed for '{tool_name}'")
|
|
84
|
+
return f"Error triggering Jenkins pipeline: {exc}"
|
|
85
|
+
|
|
86
|
+
queue_location = response.headers.get("Location", "")
|
|
87
|
+
build_info = poll_queue_for_build_number(queue_location, effective_polling, auth)
|
|
88
|
+
|
|
89
|
+
if build_info["status"] != "success":
|
|
90
|
+
return build_info["message"]
|
|
91
|
+
|
|
92
|
+
build_number: int = build_info["build_number"]
|
|
93
|
+
build_url: str = build_info["build_url"]
|
|
94
|
+
|
|
95
|
+
if not effective_polling.wait_for_completion:
|
|
96
|
+
logger.info(f"Fire-and-forget: build #{build_number} triggered at {build_url}")
|
|
97
|
+
return f"Build #{build_number} triggered: {build_url}"
|
|
98
|
+
|
|
99
|
+
job_name = extract_job_name_from_url(pipeline_cfg.uri)
|
|
100
|
+
return wait_for_build(config.base_url, job_name, build_number, effective_polling, auth)
|
|
101
|
+
|
|
102
|
+
trigger.__name__ = tool_name
|
|
103
|
+
return trigger
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def create_jenkins_tools(config: JenkinsConfig) -> list[Any]:
|
|
107
|
+
"""Generate LangChain StructuredTools from a JenkinsConfig.
|
|
108
|
+
|
|
109
|
+
For each entry in ``config.pipelines`` a StructuredTool is created whose
|
|
110
|
+
name is ``{pipeline_key}_tool`` (e.g. pipeline key ``create_workspace``
|
|
111
|
+
→ tool name ``create_workspace_tool``).
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
config: Validated JenkinsConfig loaded from jenkins.yaml.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List of pipeline StructuredTools ready for ``register_usecase_tools()``.
|
|
118
|
+
The builtin observability tools are default tools and not included here.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
tools: list[Any] = []
|
|
122
|
+
|
|
123
|
+
for pipeline_name, pipeline_cfg in config.pipelines.items():
|
|
124
|
+
tool_name = f"{pipeline_name}_tool"
|
|
125
|
+
args_schema = _build_args_schema(tool_name, pipeline_cfg)
|
|
126
|
+
trigger_fn = _make_trigger_fn(tool_name, pipeline_cfg, config)
|
|
127
|
+
description = pipeline_cfg.description or f"Trigger Jenkins pipeline: {pipeline_cfg.uri}"
|
|
128
|
+
|
|
129
|
+
tools.append(
|
|
130
|
+
StructuredTool.from_function(
|
|
131
|
+
func=trigger_fn,
|
|
132
|
+
name=tool_name,
|
|
133
|
+
description=description,
|
|
134
|
+
args_schema=args_schema,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
logger.info(f"Registered Jenkins tool '{tool_name}' → {pipeline_cfg.uri}")
|
|
138
|
+
|
|
139
|
+
return tools
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def register_pipeline_tools() -> list[Any]:
|
|
143
|
+
"""Load jenkins.yaml and return dynamic pipeline tools.
|
|
144
|
+
|
|
145
|
+
Returns an empty list when jenkins.yaml is absent or unreadable.
|
|
146
|
+
Callers are responsible for caching the result — this function always
|
|
147
|
+
reads from disk on each invocation.
|
|
148
|
+
"""
|
|
149
|
+
try:
|
|
150
|
+
from autobots_devtools_shared_lib.common.jenkins.loader import load_jenkins_config
|
|
151
|
+
|
|
152
|
+
logger.info("Loading Jenkins pipeline tools")
|
|
153
|
+
config = load_jenkins_config()
|
|
154
|
+
if config:
|
|
155
|
+
set_jenkins_config(config)
|
|
156
|
+
pipeline_tools = create_jenkins_tools(config)
|
|
157
|
+
# Builtins require a valid config (set inside create_jenkins_tools via set_jenkins_config).
|
|
158
|
+
# Include them here so they are only available when Jenkins is actually configured.
|
|
159
|
+
all_tools = [get_jenkins_build_status, get_jenkins_console_log, *pipeline_tools]
|
|
160
|
+
logger.info(
|
|
161
|
+
f"Loaded {len(pipeline_tools)} pipeline tool(s) + 2 builtin Jenkins tools"
|
|
162
|
+
" from jenkins.yaml"
|
|
163
|
+
)
|
|
164
|
+
return all_tools
|
|
165
|
+
except Exception:
|
|
166
|
+
logger.exception("Failed to load Jenkins pipeline tools — continuing without them")
|
|
167
|
+
return []
|
|
@@ -136,6 +136,9 @@ def list_files(body: ListFilesBody) -> dict[str, Any]:
|
|
|
136
136
|
"""List files under path. When workspace_context is set, path is under that workspace."""
|
|
137
137
|
set_session_id(body.session_id or "default_session_id")
|
|
138
138
|
logger.info("listFiles called path=%s", body.path)
|
|
139
|
+
# Resolve the workspace root (config.root or config.root / workspace_base_path)
|
|
140
|
+
workspace_root = _path_under_root(body.workspace_context, None)
|
|
141
|
+
# Resolve the base directory to walk, under the workspace root
|
|
139
142
|
base = _path_under_root(body.workspace_context, body.path)
|
|
140
143
|
if not base.exists():
|
|
141
144
|
logger.warning("listFiles path not found path=%s", base)
|
|
@@ -148,8 +151,10 @@ def list_files(body: ListFilesBody) -> dict[str, Any]:
|
|
|
148
151
|
for name in filenames:
|
|
149
152
|
full = Path(root) / name
|
|
150
153
|
try:
|
|
151
|
-
|
|
154
|
+
# Return paths relative to the workspace root (after the workspace context folder)
|
|
155
|
+
rel = full.relative_to(workspace_root)
|
|
152
156
|
except ValueError:
|
|
157
|
+
# Skip any files not under the workspace root
|
|
153
158
|
continue
|
|
154
159
|
files.append(str(rel).replace("\\", "/"))
|
|
155
160
|
logger.info("listFiles success path=%s count=%s", body.path, len(files))
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
# UI streaming helpers live in dynagent.ui to avoid pulling Chainlit for
|
|
6
6
|
# batch/invoke-only use.
|
|
7
7
|
|
|
8
|
+
from autobots_devtools_shared_lib.common.jenkins.config import JenkinsConfig, JenkinsPipelineConfig
|
|
9
|
+
from autobots_devtools_shared_lib.common.jenkins.loader import load_jenkins_config
|
|
10
|
+
from autobots_devtools_shared_lib.common.jenkins.tools import create_jenkins_tools
|
|
8
11
|
from autobots_devtools_shared_lib.common.utils.format_utils import output_format_converter
|
|
9
12
|
from autobots_devtools_shared_lib.dynagent.agents.agent_config_utils import get_batch_enabled_agents
|
|
10
13
|
from autobots_devtools_shared_lib.dynagent.agents.agent_meta import AgentMeta
|
|
@@ -33,15 +36,19 @@ __all__ = [
|
|
|
33
36
|
"BatchResult",
|
|
34
37
|
"Dynagent",
|
|
35
38
|
"DynagentSettings",
|
|
39
|
+
"JenkinsConfig",
|
|
40
|
+
"JenkinsPipelineConfig",
|
|
36
41
|
"LLMProvider",
|
|
37
42
|
"RecordResult",
|
|
38
43
|
"ainvoke_agent",
|
|
39
44
|
"batch_invoker",
|
|
40
45
|
"create_base_agent",
|
|
46
|
+
"create_jenkins_tools",
|
|
41
47
|
"get_batch_enabled_agents",
|
|
42
48
|
"get_dynagent_settings",
|
|
43
49
|
"invoke_agent",
|
|
44
50
|
"lm",
|
|
51
|
+
"load_jenkins_config",
|
|
45
52
|
"output_format_converter",
|
|
46
53
|
"register_usecase_tools",
|
|
47
54
|
"set_dynagent_settings",
|
|
@@ -43,11 +43,6 @@ class DynagentSettings(BaseSettings):
|
|
|
43
43
|
)
|
|
44
44
|
|
|
45
45
|
# Workspace settings
|
|
46
|
-
# TODO: Remove these settings
|
|
47
|
-
workspace_base: Path = Field(default=Path("workspace"), description="Workspace base directory")
|
|
48
|
-
schema_base: Path = Field(
|
|
49
|
-
default=Path("schemas"), description="Base directory for JSON schemas"
|
|
50
|
-
)
|
|
51
46
|
dynagent_config_root_dir: Path = Field(
|
|
52
47
|
default=Path("configs"),
|
|
53
48
|
description="Base directory for agent configuration files",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# ABOUTME: State management tools for the dynagent workflow.
|
|
2
|
-
# ABOUTME: Provides command helpers
|
|
2
|
+
# ABOUTME: Provides command helpers and handoff logic.
|
|
3
3
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
@@ -8,7 +8,6 @@ from langchain.tools import ToolRuntime, tool
|
|
|
8
8
|
from langgraph.types import Command
|
|
9
9
|
|
|
10
10
|
from autobots_devtools_shared_lib.common.observability.logging_utils import get_logger
|
|
11
|
-
from autobots_devtools_shared_lib.dynagent.config.dynagent_settings import get_dynagent_settings
|
|
12
11
|
from autobots_devtools_shared_lib.dynagent.models.state import Dynagent
|
|
13
12
|
|
|
14
13
|
logger = get_logger(__name__)
|
|
@@ -47,34 +46,14 @@ def transition_cmd(message: str, tool_call_id: str, new_agent: str, **updates: A
|
|
|
47
46
|
)
|
|
48
47
|
|
|
49
48
|
|
|
50
|
-
# --- Workspace file I/O (core logic extracted for testability) ---
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _do_write_file(session_id: str, filename: str, content: str) -> str:
|
|
54
|
-
"""Core write logic."""
|
|
55
|
-
workspace_base = get_dynagent_settings().workspace_base
|
|
56
|
-
path = workspace_base / session_id / filename
|
|
57
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
-
path.write_text(content)
|
|
59
|
-
logger.info(f"Wrote workspace file: {path}")
|
|
60
|
-
return f"Successfully wrote {filename}"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def _do_read_file(session_id: str, filename: str) -> str:
|
|
64
|
-
"""Core read logic."""
|
|
65
|
-
workspace_base = get_dynagent_settings().workspace_base
|
|
66
|
-
path = workspace_base / session_id / filename
|
|
67
|
-
if not path.exists():
|
|
68
|
-
return f"Error: file not found: workspace/{session_id}/{filename}"
|
|
69
|
-
return path.read_text()
|
|
70
|
-
|
|
71
|
-
|
|
72
49
|
# --- Handoff validation (extracted for testability) ---
|
|
73
50
|
|
|
74
51
|
|
|
75
52
|
def _validate_handoff(next_agent: str) -> str | None:
|
|
76
53
|
"""Validate agent name against config. Returns error string or None."""
|
|
77
|
-
from autobots_devtools_shared_lib.dynagent.agents.agent_config_utils import
|
|
54
|
+
from autobots_devtools_shared_lib.dynagent.agents.agent_config_utils import (
|
|
55
|
+
get_agent_list,
|
|
56
|
+
)
|
|
78
57
|
|
|
79
58
|
valid = get_agent_list()
|
|
80
59
|
if next_agent not in valid:
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# ABOUTME: Central registry of all dynagent-layer tools plus usecase-registered pools.
|
|
2
2
|
# ABOUTME: Default tools live here; use-cases (e.g. BRO) register their own at startup.
|
|
3
3
|
|
|
4
|
+
import threading
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
7
|
+
from autobots_devtools_shared_lib.common.observability.logging_utils import get_logger
|
|
6
8
|
from autobots_devtools_shared_lib.common.tools.context_tools import (
|
|
7
9
|
clear_context_tool,
|
|
8
10
|
get_context_tool,
|
|
@@ -20,10 +22,21 @@ from autobots_devtools_shared_lib.common.tools.fserver_client_tools import (
|
|
|
20
22
|
)
|
|
21
23
|
from autobots_devtools_shared_lib.dynagent.tools.state_tools import get_agent_list, handoff
|
|
22
24
|
|
|
25
|
+
logger = get_logger(__name__)
|
|
26
|
+
|
|
23
27
|
# --- Module-level usecase storage (populated by register_* at startup) ---
|
|
24
28
|
|
|
25
29
|
_USECASE_TOOLS: list[Any] = []
|
|
26
30
|
|
|
31
|
+
# --- Jenkins pipeline tool cache (None = not yet loaded) ---
|
|
32
|
+
# Loaded once on first get_jenkins_usecase_tools() call.
|
|
33
|
+
# [] means jenkins.yaml absent/unreadable — permanently cached, never retried.
|
|
34
|
+
# Double-checked locking guards against concurrent initialisation during I/O
|
|
35
|
+
# (CPython releases the GIL on file reads, making the race real).
|
|
36
|
+
|
|
37
|
+
_jenkins_tools_cache: list[Any] | None = None
|
|
38
|
+
_jenkins_tools_lock = threading.Lock()
|
|
39
|
+
|
|
27
40
|
|
|
28
41
|
# --- Default (dynagent-layer) tools ---
|
|
29
42
|
|
|
@@ -47,6 +60,28 @@ def get_default_tools() -> list[Any]:
|
|
|
47
60
|
]
|
|
48
61
|
|
|
49
62
|
|
|
63
|
+
def get_jenkins_usecase_tools() -> list[Any]:
|
|
64
|
+
"""Return Jenkins pipeline tools discovered from jenkins.yaml.
|
|
65
|
+
|
|
66
|
+
Thread-safe via double-checked locking:
|
|
67
|
+
- Outer check avoids lock acquisition on every call once initialised (fast path).
|
|
68
|
+
- Inner check inside the lock prevents duplicate initialisation when two threads
|
|
69
|
+
race through the outer check simultaneously during I/O (GIL released on reads).
|
|
70
|
+
Returns [] permanently if jenkins.yaml is absent or unreadable.
|
|
71
|
+
"""
|
|
72
|
+
logger.info("Getting Jenkins pipeline tools")
|
|
73
|
+
global _jenkins_tools_cache
|
|
74
|
+
if _jenkins_tools_cache is None: # 1st check — no lock (fast path)
|
|
75
|
+
with _jenkins_tools_lock: # acquire lock
|
|
76
|
+
if _jenkins_tools_cache is None: # 2nd check — inside lock (safe)
|
|
77
|
+
from autobots_devtools_shared_lib.common.jenkins.tools import (
|
|
78
|
+
register_pipeline_tools,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
_jenkins_tools_cache = register_pipeline_tools()
|
|
82
|
+
return _jenkins_tools_cache
|
|
83
|
+
|
|
84
|
+
|
|
50
85
|
# --- Usecase registration (called once per use-case at startup) ---
|
|
51
86
|
|
|
52
87
|
|
|
@@ -64,12 +99,15 @@ def get_usecase_tools() -> list[Any]:
|
|
|
64
99
|
|
|
65
100
|
|
|
66
101
|
def get_all_tools() -> list[Any]:
|
|
67
|
-
"""Return default +
|
|
68
|
-
|
|
102
|
+
"""Return default + jenkins pipeline + usecase tools. Pure reader."""
|
|
103
|
+
logger.info("Getting all tools")
|
|
104
|
+
return get_default_tools() + get_jenkins_usecase_tools() + get_usecase_tools()
|
|
69
105
|
|
|
70
106
|
|
|
71
107
|
# --- Test-isolation helpers (private; used only by fixtures) ---
|
|
72
108
|
|
|
73
109
|
|
|
74
110
|
def _reset_usecase_tools() -> None:
|
|
111
|
+
global _jenkins_tools_cache
|
|
75
112
|
_USECASE_TOOLS.clear()
|
|
113
|
+
_jenkins_tools_cache = None
|
|
@@ -47,7 +47,7 @@ logger = get_logger(__name__)
|
|
|
47
47
|
@cl.on_chat_start
|
|
48
48
|
async def start():
|
|
49
49
|
"""Create the base agent once and store it in the Chainlit session."""
|
|
50
|
-
agent = create_base_agent(
|
|
50
|
+
agent = create_base_agent() # pyright: ignore[reportCallIssue]
|
|
51
51
|
cl.user_session.set("agent", agent)
|
|
52
52
|
await cl.Message(content="Hello, how can I help you today?").send()
|
|
53
53
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|