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.
Files changed (64) hide show
  1. {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/PKG-INFO +1 -1
  2. {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/pyproject.toml +1 -1
  3. autobots_devtools_shared_lib-0.2.3a2/src/autobots_devtools_shared_lib/common/jenkins/__init__.py +26 -0
  4. autobots_devtools_shared_lib-0.2.3a2/src/autobots_devtools_shared_lib/common/jenkins/builtin_tools.py +89 -0
  5. autobots_devtools_shared_lib-0.2.3a2/src/autobots_devtools_shared_lib/common/jenkins/config.py +105 -0
  6. autobots_devtools_shared_lib-0.2.3a2/src/autobots_devtools_shared_lib/common/jenkins/http_utils.py +147 -0
  7. autobots_devtools_shared_lib-0.2.3a2/src/autobots_devtools_shared_lib/common/jenkins/loader.py +47 -0
  8. autobots_devtools_shared_lib-0.2.3a2/src/autobots_devtools_shared_lib/common/jenkins/tools.py +167 -0
  9. {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
  10. {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/dynagent/__init__.py +7 -0
  11. {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
  12. {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
  13. {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
  14. {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
  15. {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/README.md +0 -0
  16. {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/__init__.py +0 -0
  17. {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/common/__init__.py +0 -0
  18. {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
  19. {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
  20. {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
  21. {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
  22. {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
  23. {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
  24. {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
  25. {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
  26. {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
  27. {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
  28. {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
  29. {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
  30. {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
  31. {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
  32. {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
  33. {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
  34. {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
  35. {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
  36. {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
  37. {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
  38. {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
  39. {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
  40. {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
  41. {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
  42. {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
  43. {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
  44. {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
  45. {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
  46. {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
  47. {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
  48. {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
  49. {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
  50. {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
  51. {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
  52. {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
  53. {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
  54. {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
  55. {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
  56. {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
  57. {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
  58. {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
  59. {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
  60. {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
  61. {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
  62. {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
  63. {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
  64. {autobots_devtools_shared_lib-0.2.1 → autobots_devtools_shared_lib-0.2.3a2}/src/autobots_devtools_shared_lib/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autobots-devtools-shared-lib
3
- Version: 0.2.1
3
+ Version: 0.2.3a2
4
4
  Summary: Shared library functions to be used for all autobots projects
5
5
  License: MIT
6
6
  Author: Pralhad
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "autobots-devtools-shared-lib"
3
- version = "0.2.1"
3
+ version = "0.2.3-alpha.2"
4
4
  description = "Shared library functions to be used for all autobots projects"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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
@@ -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
+ )
@@ -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
@@ -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
- rel = full.relative_to(config.root)
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, workspace file I/O, and handoff logic.
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 get_agent_list
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 + usecase tools (the full pool passed to the agent)."""
68
- return get_default_tools() + get_usecase_tools()
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(initial_agent_name="coordinator") # pyright: ignore[reportCallIssue]
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