autobots-devtools-shared-lib 0.3.2__tar.gz → 0.3.4__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 (70) hide show
  1. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/PKG-INFO +1 -1
  2. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/pyproject.toml +1 -1
  3. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/config/jenkins_config.py +22 -10
  4. autobots_devtools_shared_lib-0.3.4/src/autobots_devtools_shared_lib/common/config/jenkins_constants.py +65 -0
  5. autobots_devtools_shared_lib-0.3.4/src/autobots_devtools_shared_lib/common/config/jenkins_loader.py +75 -0
  6. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/observability/otel_fastapi.py +12 -6
  7. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/observability/trace_propagation.py +37 -5
  8. autobots_devtools_shared_lib-0.3.4/src/autobots_devtools_shared_lib/common/tools/jenkins_builtin_tools.py +35 -0
  9. autobots_devtools_shared_lib-0.3.4/src/autobots_devtools_shared_lib/common/tools/jenkins_pipeline_tools.py +117 -0
  10. autobots_devtools_shared_lib-0.3.4/src/autobots_devtools_shared_lib/common/utils/jenkins_builtin_utils.py +97 -0
  11. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/utils/jenkins_http_utils.py +15 -6
  12. autobots_devtools_shared_lib-0.3.4/src/autobots_devtools_shared_lib/common/utils/jenkins_pipeline_utils.py +217 -0
  13. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/agents/batch.py +6 -0
  14. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/agents/invocation_utils.py +147 -105
  15. autobots_devtools_shared_lib-0.3.2/src/autobots_devtools_shared_lib/common/config/jenkins_loader.py +0 -47
  16. autobots_devtools_shared_lib-0.3.2/src/autobots_devtools_shared_lib/common/tools/jenkins_builtin_tools.py +0 -89
  17. autobots_devtools_shared_lib-0.3.2/src/autobots_devtools_shared_lib/common/tools/jenkins_pipeline_tools.py +0 -167
  18. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/README.md +0 -0
  19. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/__init__.py +0 -0
  20. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/__init__.py +0 -0
  21. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/config/__init__.py +0 -0
  22. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/observability/__init__.py +0 -0
  23. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/observability/logging_utils.py +0 -0
  24. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/observability/trace_metadata.py +0 -0
  25. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/observability/tracing.py +0 -0
  26. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/servers/__init__.py +0 -0
  27. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/servers/fileserver/README.md +0 -0
  28. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/servers/fileserver/__init__.py +0 -0
  29. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/servers/fileserver/app.py +0 -0
  30. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/servers/fileserver/config.py +0 -0
  31. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/servers/fileserver/models.py +0 -0
  32. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/services/__init__.py +0 -0
  33. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/services/context/README.md +0 -0
  34. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/services/context/__init__.py +0 -0
  35. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/services/context/cache_backed.py +0 -0
  36. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/services/context/db_repository.py +0 -0
  37. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/services/context/factory.py +0 -0
  38. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/services/context/in_memory.py +0 -0
  39. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/services/context/redis_store.py +0 -0
  40. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/services/context/store.py +0 -0
  41. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/tools/__init__.py +0 -0
  42. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/tools/context_tools.py +0 -0
  43. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/tools/format_tools.py +0 -0
  44. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/tools/fserver_client_tools.py +0 -0
  45. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/utils/__init__.py +0 -0
  46. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/utils/context_utils.py +0 -0
  47. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/utils/format_utils.py +0 -0
  48. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/common/utils/fserver_client_utils.py +0 -0
  49. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/__init__.py +0 -0
  50. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/agents/__init__.py +0 -0
  51. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/agents/agent_config_utils.py +0 -0
  52. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/agents/agent_meta.py +0 -0
  53. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/agents/base_agent.py +0 -0
  54. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/agents/middleware.py +0 -0
  55. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/config/__init__.py +0 -0
  56. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/config/dynagent_settings.py +0 -0
  57. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/llm/__init__.py +0 -0
  58. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/llm/llm.py +0 -0
  59. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/models/__init__.py +0 -0
  60. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/models/state.py +0 -0
  61. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/services/__init__.py +0 -0
  62. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/services/structured_converter.py +0 -0
  63. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/tools/__init__.py +0 -0
  64. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/tools/state_tools.py +0 -0
  65. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/tools/tool_registry.py +0 -0
  66. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/ui/__init__.py +0 -0
  67. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/ui/default_ui.py +0 -0
  68. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/ui/ui_utils.py +0 -0
  69. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/src/autobots_devtools_shared_lib/dynagent/utils/__init__.py +0 -0
  70. {autobots_devtools_shared_lib-0.3.2 → autobots_devtools_shared_lib-0.3.4}/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.3.2
3
+ Version: 0.3.4
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.3.2"
3
+ version = "0.3.4"
4
4
  description = "Shared library functions to be used for all autobots projects"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -5,16 +5,28 @@ from __future__ import annotations
5
5
 
6
6
  from pydantic import BaseModel, Field
7
7
 
8
+ from autobots_devtools_shared_lib.common.config.jenkins_constants import (
9
+ DEFAULT_API_TOKEN_ENV,
10
+ DEFAULT_MAX_WAIT_SECONDS,
11
+ DEFAULT_PARAM_REQUIRED,
12
+ DEFAULT_PARAM_TYPE,
13
+ DEFAULT_POLL_INTERVAL_SECONDS,
14
+ DEFAULT_QUEUE_MAX_RETRIES,
15
+ DEFAULT_QUEUE_RETRY_DELAY_SECONDS,
16
+ DEFAULT_USERNAME_ENV,
17
+ DEFAULT_WAIT_FOR_COMPLETION,
18
+ )
19
+
8
20
 
9
21
  class JenkinsAuthConfig(BaseModel):
10
22
  """Credentials resolved from environment variables at runtime."""
11
23
 
12
24
  username_env: str = Field(
13
- default="JENKINS_USERNAME",
25
+ default=DEFAULT_USERNAME_ENV,
14
26
  description="Name of the env var holding the Jenkins username",
15
27
  )
16
28
  token_env: str = Field(
17
- default="JENKINS_API_TOKEN",
29
+ default=DEFAULT_API_TOKEN_ENV,
18
30
  description="Name of the env var holding the Jenkins API token",
19
31
  )
20
32
 
@@ -23,23 +35,23 @@ class JenkinsPollingConfig(BaseModel):
23
35
  """Controls whether and how long to wait for a triggered build to complete."""
24
36
 
25
37
  wait_for_completion: bool = Field(
26
- default=True,
38
+ default=DEFAULT_WAIT_FOR_COMPLETION,
27
39
  description="Whether to block until the build finishes",
28
40
  )
29
41
  poll_interval_seconds: int = Field(
30
- default=10,
42
+ default=DEFAULT_POLL_INTERVAL_SECONDS,
31
43
  description="Seconds to wait between build-status polls",
32
44
  )
33
45
  max_wait_seconds: int = Field(
34
- default=300,
46
+ default=DEFAULT_MAX_WAIT_SECONDS,
35
47
  description="Maximum seconds to wait before giving up",
36
48
  )
37
49
  queue_max_retries: int = Field(
38
- default=5,
50
+ default=DEFAULT_QUEUE_MAX_RETRIES,
39
51
  description="Max attempts to resolve a build number from the Jenkins queue",
40
52
  )
41
53
  queue_retry_delay_seconds: int = Field(
42
- default=2,
54
+ default=DEFAULT_QUEUE_RETRY_DELAY_SECONDS,
43
55
  description="Seconds to wait between queue-item polls",
44
56
  )
45
57
 
@@ -48,7 +60,7 @@ class JenkinsParameterConfig(BaseModel):
48
60
  """A single parameter that will be forwarded as a query string to Jenkins."""
49
61
 
50
62
  type: str = Field(
51
- default="string",
63
+ default=DEFAULT_PARAM_TYPE,
52
64
  description="Parameter type: 'string', 'boolean', or 'integer'",
53
65
  )
54
66
  description: str = Field(
@@ -56,7 +68,7 @@ class JenkinsParameterConfig(BaseModel):
56
68
  description="LLM-facing hint describing what value to provide",
57
69
  )
58
70
  required: bool = Field(
59
- default=True,
71
+ default=DEFAULT_PARAM_REQUIRED,
60
72
  description="Whether the LLM must always supply this parameter",
61
73
  )
62
74
 
@@ -64,7 +76,7 @@ class JenkinsParameterConfig(BaseModel):
64
76
  class JenkinsPipelineConfig(BaseModel):
65
77
  """Configuration for a single Jenkins pipeline entry.
66
78
 
67
- The framework registers this as a LangChain tool named ``{key}_tool``,
79
+ The framework registers this as a LangChain tool named ``{key}{TOOL_NAME_SUFFIX}``,
68
80
  so pipeline keys in jenkins.yaml should not carry a ``_tool`` suffix.
69
81
  """
70
82
 
@@ -0,0 +1,65 @@
1
+ # ABOUTME: Shared constants for all Jenkins integration files.
2
+ # ABOUTME: Single source of truth for default values, magic numbers, and string literals.
3
+
4
+ from __future__ import annotations
5
+
6
+ # ---------------------------------------------------------------------------
7
+ # Authentication defaults
8
+ # ---------------------------------------------------------------------------
9
+
10
+ DEFAULT_USERNAME_ENV: str = "JENKINS_USERNAME"
11
+ DEFAULT_API_TOKEN_ENV: str = "JENKINS_API_TOKEN" # noqa: S105
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Polling defaults
15
+ # ---------------------------------------------------------------------------
16
+
17
+ DEFAULT_WAIT_FOR_COMPLETION: bool = True
18
+ DEFAULT_POLL_INTERVAL_SECONDS: int = 10
19
+ DEFAULT_MAX_WAIT_SECONDS: int = 300
20
+ DEFAULT_QUEUE_MAX_RETRIES: int = 5
21
+ DEFAULT_QUEUE_RETRY_DELAY_SECONDS: int = 2
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # HTTP
25
+ # ---------------------------------------------------------------------------
26
+
27
+ HTTP_TIMEOUT_SECONDS: int = 30
28
+ QUEUE_INITIAL_DELAY_SECONDS: int = 1
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Jenkins API paths / URL segments
32
+ # ---------------------------------------------------------------------------
33
+
34
+ JENKINS_CONFIG_FILENAME: str = "jenkins.yaml"
35
+ API_JSON_SUFFIX: str = "api/json"
36
+ JOB_URL_SEGMENT: str = "job"
37
+ LAST_BUILD_SEGMENT: str = "lastBuild"
38
+ CONSOLE_LOG_PATH: str = "logText/progressiveText"
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # URL templates (use .format(base_url=..., job_name=..., build_number=..., start=...))
42
+ # ---------------------------------------------------------------------------
43
+
44
+ _BASE_JOB_PREFIX: str = "{base_url}/" + JOB_URL_SEGMENT + "/{job_name}/"
45
+
46
+ BUILD_STATUS_LATEST_URL: str = _BASE_JOB_PREFIX + LAST_BUILD_SEGMENT + "/" + API_JSON_SUFFIX
47
+ BUILD_STATUS_URL: str = _BASE_JOB_PREFIX + "{build_number}/" + API_JSON_SUFFIX
48
+ CONSOLE_LOG_LATEST_URL: str = (
49
+ _BASE_JOB_PREFIX + LAST_BUILD_SEGMENT + "/" + CONSOLE_LOG_PATH + "?start={start}"
50
+ )
51
+ CONSOLE_LOG_URL: str = _BASE_JOB_PREFIX + "{build_number}/" + CONSOLE_LOG_PATH + "?start={start}"
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Tool naming
55
+ # ---------------------------------------------------------------------------
56
+
57
+ TOOL_NAME_SUFFIX: str = "_tool"
58
+ ARGS_SCHEMA_SUFFIX: str = "_args"
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Parameter config defaults
62
+ # ---------------------------------------------------------------------------
63
+
64
+ DEFAULT_PARAM_TYPE: str = "string"
65
+ DEFAULT_PARAM_REQUIRED: bool = True
@@ -0,0 +1,75 @@
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
+ # ABOUTME: Owns the JenkinsConfig singleton — get_jenkins_config() is the shared access point.
4
+
5
+ from __future__ import annotations
6
+
7
+ import yaml
8
+
9
+ from autobots_devtools_shared_lib.common.config.jenkins_config import JenkinsConfig
10
+ from autobots_devtools_shared_lib.common.config.jenkins_constants import JENKINS_CONFIG_FILENAME
11
+ from autobots_devtools_shared_lib.common.observability.logging_utils import get_logger
12
+
13
+ logger = get_logger(__name__)
14
+
15
+ _config: JenkinsConfig | None = None
16
+ _config_loaded: bool = False
17
+
18
+
19
+ def set_jenkins_config(config: JenkinsConfig | None) -> None:
20
+ """Explicitly set the Jenkins config singleton (useful for tests or custom injection)."""
21
+ global _config, _config_loaded
22
+ _config = config
23
+ _config_loaded = True
24
+
25
+
26
+ def get_jenkins_config() -> JenkinsConfig | None:
27
+ """Return the cached JenkinsConfig, loading from disk on the first call.
28
+
29
+ Jenkins config is optional — returns None when jenkins.yaml is absent.
30
+ The result (including None) is cached after the first call so the filesystem
31
+ is only read once regardless of outcome. Subsequent calls return immediately.
32
+
33
+ Raises:
34
+ yaml.YAMLError: If the file contains invalid YAML (first call only).
35
+ pydantic.ValidationError: If the YAML structure does not match the schema (first call only).
36
+ """
37
+ global _config, _config_loaded
38
+ if not _config_loaded:
39
+ set_jenkins_config(_load_jenkins_config())
40
+ return _config
41
+
42
+
43
+ def _load_jenkins_config() -> JenkinsConfig | None:
44
+ """Load and validate jenkins.yaml from the active config directory.
45
+
46
+ Internal implementation — always reads from disk without caching.
47
+ Use get_jenkins_config() for the cached singleton instead.
48
+
49
+ Returns:
50
+ Validated JenkinsConfig or None if the file does not exist.
51
+
52
+ Raises:
53
+ yaml.YAMLError: If the file contains invalid YAML.
54
+ pydantic.ValidationError: If the YAML structure does not match the schema.
55
+ """
56
+ from autobots_devtools_shared_lib.dynagent.agents.agent_config_utils import get_config_dir
57
+
58
+ config_dir = get_config_dir()
59
+ jenkins_path = config_dir / JENKINS_CONFIG_FILENAME
60
+
61
+ if not jenkins_path.exists():
62
+ logger.debug(
63
+ f"No jenkins.yaml found at {jenkins_path}; Jenkins tools will not be registered"
64
+ )
65
+ return None
66
+
67
+ logger.info(f"Loading Jenkins config from {jenkins_path}")
68
+ with open(jenkins_path) as f: # noqa: PTH123
69
+ raw = yaml.safe_load(f)
70
+
71
+ config = JenkinsConfig.model_validate(raw["jenkins_config"])
72
+ logger.info(
73
+ f"Loaded Jenkins config with {len(config.pipelines)} pipeline(s): {list(config.pipelines)}"
74
+ )
75
+ return config
@@ -34,14 +34,20 @@ class OtelCaptureConfig:
34
34
  ]
35
35
 
36
36
 
37
+ # Default Langfuse cloud host when LANGFUSE_HOST is not set (same as trace_propagation.py).
38
+ _DEFAULT_LANGFUSE_HOST = "https://cloud.langfuse.com"
39
+
40
+
37
41
  def _configure_langfuse_otlp() -> bool:
38
42
  """If Langfuse keys are set, configure env for OTLP export to Langfuse. Returns True if set."""
39
43
  pk = os.getenv("LANGFUSE_PUBLIC_KEY", "").strip()
40
44
  sk = os.getenv("LANGFUSE_SECRET_KEY", "").strip()
41
45
  if not pk or not sk:
42
46
  return False
43
- host = os.getenv("LANGFUSE_HOST")
44
- endpoint = f"{host}/api/public/otel"
47
+ host = os.getenv("LANGFUSE_BASE_URL", _DEFAULT_LANGFUSE_HOST).strip()
48
+ if not host:
49
+ return False
50
+ endpoint = f"{host.rstrip('/')}/api/public/otel"
45
51
  auth = base64.b64encode(f"{pk}:{sk}".encode()).decode()
46
52
  os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = endpoint
47
53
  os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"Authorization=Basic {auth}"
@@ -207,12 +213,12 @@ def _set_span_attributes(
207
213
  input_data["body"] = req_truncated["content"]
208
214
  input_data["body_size"] = req_truncated["size_bytes"]
209
215
 
210
- # Extract conversation_id and set as langfuse.session.id
216
+ # Extract session_id and set as langfuse.session.id
211
217
  try:
212
218
  req_json = json.loads(req_body.decode("utf-8"))
213
- conversation_id = req_json.get("conversation_id")
214
- if conversation_id:
215
- span.set_attribute("langfuse.session.id", str(conversation_id))
219
+ session_id = req_json.get("session_id")
220
+ if session_id:
221
+ span.set_attribute("langfuse.session.id", str(session_id))
216
222
  except (json.JSONDecodeError, UnicodeDecodeError):
217
223
  # Not JSON or invalid UTF-8, skip session linking
218
224
  pass
@@ -2,7 +2,7 @@
2
2
  OpenTelemetry trace propagation helpers for HTTP client calls.
3
3
 
4
4
  Provides W3C traceparent header injection and session linking for fileserver HTTP calls.
5
- Reuses Langfuse configuration (LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_HOST).
5
+ Reuses Langfuse configuration (LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_BASE_URL).
6
6
  """
7
7
 
8
8
  from __future__ import annotations
@@ -11,7 +11,7 @@ import base64
11
11
  import os
12
12
  import sys
13
13
  from contextlib import contextmanager
14
- from typing import TYPE_CHECKING
14
+ from typing import TYPE_CHECKING, Any
15
15
 
16
16
  if TYPE_CHECKING:
17
17
  from collections.abc import Generator
@@ -43,8 +43,8 @@ def _ensure_tracer_provider() -> bool:
43
43
  try:
44
44
  # Check if OTEL packages available
45
45
  from opentelemetry import trace
46
- from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
47
- OTLPSpanExporter, # pyright: ignore[reportMissingImports]
46
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( # pyright: ignore[reportMissingImports]
47
+ OTLPSpanExporter,
48
48
  )
49
49
  from opentelemetry.sdk.resources import Resource
50
50
  from opentelemetry.sdk.trace import TracerProvider
@@ -80,7 +80,7 @@ def _ensure_tracer_provider() -> bool:
80
80
 
81
81
  try:
82
82
  # Configure OTLP endpoint (same pattern as otel_fastapi.py)
83
- host = os.getenv("LANGFUSE_HOST", "https://cloud.langfuse.com")
83
+ host = os.getenv("LANGFUSE_BASE_URL", "https://cloud.langfuse.com")
84
84
  endpoint = f"{host}/api/public/otel"
85
85
  auth = base64.b64encode(f"{pk}:{sk}".encode()).decode()
86
86
  os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = endpoint
@@ -115,6 +115,35 @@ def _ensure_tracer_provider() -> bool:
115
115
  return False
116
116
 
117
117
 
118
+ def ensure_tracer_provider() -> bool:
119
+ """Public wrapper — returns True when an OTel tracer provider is ready."""
120
+ return _ensure_tracer_provider()
121
+
122
+
123
+ def otel_span(name: str, kind: str = "SERVER") -> Any:
124
+ """Return an OTel span context manager, or nullcontext if OTel is unavailable.
125
+
126
+ Use this to create a root span that encompasses an entire orchestration step
127
+ (agent invocation + downstream file I/O) so all child spans share one trace.
128
+
129
+ Args:
130
+ name: Span name shown in Langfuse.
131
+ kind: OTel SpanKind string — "SERVER" (default) or "CLIENT".
132
+ """
133
+ from contextlib import nullcontext
134
+
135
+ if not _ensure_tracer_provider():
136
+ return nullcontext()
137
+ try:
138
+ from opentelemetry import trace
139
+ from opentelemetry.trace import SpanKind
140
+
141
+ span_kind = SpanKind.CLIENT if kind == "CLIENT" else SpanKind.SERVER
142
+ return trace.get_tracer(__name__).start_as_current_span(name, kind=span_kind)
143
+ except Exception:
144
+ return nullcontext()
145
+
146
+
118
147
  @contextmanager
119
148
  def traced_http_call(
120
149
  operation: str,
@@ -125,6 +154,9 @@ def traced_http_call(
125
154
  Context manager that creates an OTEL client span and injects W3C traceparent header.
126
155
 
127
156
  Links HTTP calls to Langfuse sessions via langfuse.session.id attribute.
157
+ When the same session_id is used for the parent (e.g. nurture trigger / agent
158
+ invocation) and for file client calls, Langfuse groups them in the Session view
159
+ so http.client.readFile / http.client.writeFile appear with the agent trace.
128
160
  Gracefully degrades to empty headers if OTEL unavailable.
129
161
 
130
162
  Args:
@@ -0,0 +1,35 @@
1
+ # ABOUTME: Thin LangChain agent layer for Jenkins observability tools.
2
+ # ABOUTME: Wraps get_build_status() and get_console_log() from jenkins_builtin_utils as @tools.
3
+ # ABOUTME: All HTTP logic lives in common/utils/jenkins_builtin_utils.py.
4
+
5
+ from __future__ import annotations
6
+
7
+ from langchain.tools import tool
8
+
9
+ from autobots_devtools_shared_lib.common.utils.jenkins_builtin_utils import (
10
+ get_build_status,
11
+ get_console_log,
12
+ )
13
+
14
+
15
+ @tool
16
+ def get_jenkins_build_status(job_name: str, build_number: int | None = None) -> str:
17
+ """Get the current status of a Jenkins build.
18
+
19
+ Returns a concise string with job name, build number,
20
+ status (SUCCESS / FAILURE / IN_PROGRESS), and build URL.
21
+ Omit build_number to check the latest build.
22
+ """
23
+ return get_build_status(job_name, build_number)
24
+
25
+
26
+ @tool
27
+ def get_jenkins_console_log(job_name: str, build_number: int | None = None, start: int = 0) -> str:
28
+ """Retrieve the console log output for a Jenkins build.
29
+
30
+ Returns the log text from the given byte offset.
31
+ Use start=0 (default) for the full log.
32
+ Omit build_number to use the latest build.
33
+ Useful for diagnosing pipeline failures.
34
+ """
35
+ return get_console_log(job_name, build_number, start)
@@ -0,0 +1,117 @@
1
+ # ABOUTME: Thin LangChain agent layer — wraps JenkinsPipelineRunner as StructuredTools.
2
+ # ABOUTME: All execution logic lives in common/utils/jenkins_pipeline_utils.py.
3
+ # ABOUTME: register_pipeline_tools() is the single entry point for agent tool registration.
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from langchain_core.tools import StructuredTool
10
+ from pydantic import BaseModel, Field, create_model
11
+
12
+ from autobots_devtools_shared_lib.common.config.jenkins_constants import (
13
+ ARGS_SCHEMA_SUFFIX,
14
+ TOOL_NAME_SUFFIX,
15
+ )
16
+ from autobots_devtools_shared_lib.common.observability.logging_utils import get_logger
17
+ from autobots_devtools_shared_lib.common.tools.jenkins_builtin_tools import (
18
+ get_jenkins_build_status,
19
+ get_jenkins_console_log,
20
+ )
21
+ from autobots_devtools_shared_lib.common.utils.jenkins_pipeline_utils import get_pipeline_runner
22
+
23
+ if TYPE_CHECKING:
24
+ from autobots_devtools_shared_lib.common.config.jenkins_config import JenkinsPipelineConfig
25
+
26
+ logger = get_logger(__name__)
27
+
28
+ _PYTHON_TYPE_MAP: dict[str, type] = {
29
+ "string": str,
30
+ "str": str,
31
+ "boolean": bool,
32
+ "bool": bool,
33
+ "integer": int,
34
+ "int": int,
35
+ "float": float,
36
+ "number": float,
37
+ }
38
+
39
+
40
+ def _build_args_schema(tool_name: str, pipeline_cfg: JenkinsPipelineConfig) -> type[BaseModel]:
41
+ """Build a dynamic Pydantic model from a pipeline's parameters.
42
+
43
+ Required parameters get a plain Field; optional ones get ``type | None`` with default None.
44
+ """
45
+ field_definitions: dict[str, Any] = {}
46
+ for param_name, param_cfg in pipeline_cfg.parameters.items():
47
+ py_type = _PYTHON_TYPE_MAP.get(param_cfg.type.lower(), str)
48
+ if param_cfg.required:
49
+ field_definitions[param_name] = (py_type, Field(description=param_cfg.description))
50
+ else:
51
+ field_definitions[param_name] = (
52
+ py_type | None,
53
+ Field(default=None, description=param_cfg.description),
54
+ )
55
+ return create_model(f"{tool_name}{ARGS_SCHEMA_SUFFIX}", **field_definitions)
56
+
57
+
58
+ def create_jenkins_tools() -> list[Any]:
59
+ """Generate LangChain StructuredTools from the loaded JenkinsConfig.
60
+
61
+ For each entry in ``config.pipelines`` a StructuredTool is created whose
62
+ name is ``{pipeline_key}_tool`` (e.g. pipeline key ``create_workspace``
63
+ → tool name ``create_workspace_tool``).
64
+
65
+ Config is loaded lazily via get_pipeline_runner() — no explicit config
66
+ argument needed. Call register_pipeline_tools() as the higher-level entry
67
+ point which also includes the builtin observability tools.
68
+
69
+ Returns:
70
+ List of pipeline StructuredTools ready for ``register_usecase_tools()``.
71
+ The builtin observability tools are not included here.
72
+
73
+ Raises:
74
+ RuntimeError: If jenkins.yaml cannot be found.
75
+ """
76
+ runner = get_pipeline_runner()
77
+ tools: list[Any] = []
78
+
79
+ for pipeline_name, pipeline_cfg in runner._config.pipelines.items():
80
+ tool_name = f"{pipeline_name}{TOOL_NAME_SUFFIX}"
81
+ args_schema = _build_args_schema(tool_name, pipeline_cfg)
82
+ description = pipeline_cfg.description or f"Trigger Jenkins pipeline: {pipeline_cfg.uri}"
83
+
84
+ tools.append(
85
+ StructuredTool.from_function(
86
+ func=runner.get_callable(pipeline_name),
87
+ name=tool_name,
88
+ description=description,
89
+ args_schema=args_schema,
90
+ )
91
+ )
92
+ logger.info(f"Registered Jenkins tool '{tool_name}' → {pipeline_cfg.uri}")
93
+
94
+ return tools
95
+
96
+
97
+ def register_pipeline_tools() -> list[Any]:
98
+ """Load jenkins.yaml and return all Jenkins tools (pipeline + builtin observability).
99
+
100
+ Returns an empty list when jenkins.yaml is absent — Jenkins integration is optional.
101
+ Config loading and caching is handled internally; no setup required before calling.
102
+ """
103
+ try:
104
+ from autobots_devtools_shared_lib.common.config.jenkins_loader import get_jenkins_config
105
+
106
+ if get_jenkins_config() is not None:
107
+ logger.info("Loading Jenkins pipeline tools")
108
+ pipeline_tools = create_jenkins_tools()
109
+ all_tools = [get_jenkins_build_status, get_jenkins_console_log, *pipeline_tools]
110
+ logger.info(
111
+ f"Loaded {len(pipeline_tools)} pipeline tool(s) + 2 builtin Jenkins tools"
112
+ " from jenkins.yaml"
113
+ )
114
+ return all_tools
115
+ except Exception:
116
+ logger.exception("Failed to load Jenkins pipeline tools — continuing without them")
117
+ return []
@@ -0,0 +1,97 @@
1
+ # ABOUTME: Deterministic Jenkins observability utilities — pure Python, no LangChain dependency.
2
+ # ABOUTME: Provides get_build_status() and get_console_log() for direct/deterministic use.
3
+ # ABOUTME: LangChain @tool wrappers live in common/tools/jenkins_builtin_tools.py.
4
+
5
+ from __future__ import annotations
6
+
7
+ import requests
8
+
9
+ from autobots_devtools_shared_lib.common.config.jenkins_constants import (
10
+ BUILD_STATUS_LATEST_URL,
11
+ BUILD_STATUS_URL,
12
+ CONSOLE_LOG_LATEST_URL,
13
+ CONSOLE_LOG_URL,
14
+ HTTP_TIMEOUT_SECONDS,
15
+ )
16
+ from autobots_devtools_shared_lib.common.config.jenkins_loader import get_jenkins_config
17
+ from autobots_devtools_shared_lib.common.observability.logging_utils import get_logger
18
+ from autobots_devtools_shared_lib.common.utils.jenkins_http_utils import get_auth
19
+
20
+ logger = get_logger(__name__)
21
+
22
+
23
+ def get_build_status(job_name: str, build_number: int | None = None) -> str:
24
+ """Get the current status of a Jenkins build.
25
+
26
+ Args:
27
+ job_name: Jenkins job name.
28
+ build_number: Specific build number; omit to check the latest build.
29
+
30
+ Returns:
31
+ A concise string: ``job=<name> build=<n> status=<STATUS> url=<url>``.
32
+ Returns an error string if Jenkins is not configured or the request fails.
33
+ """
34
+ config = get_jenkins_config()
35
+ if config is None:
36
+ return "Error: Jenkins is not configured (jenkins.yaml not found)"
37
+
38
+ base_url = config.base_url.rstrip("/")
39
+ auth = get_auth(config)
40
+ if build_number is None:
41
+ api_url = BUILD_STATUS_LATEST_URL.format(base_url=base_url, job_name=job_name)
42
+ else:
43
+ api_url = BUILD_STATUS_URL.format(
44
+ base_url=base_url, job_name=job_name, build_number=build_number
45
+ )
46
+
47
+ logger.info("Getting build status for job='%s' build=%s", job_name, build_number)
48
+ try:
49
+ resp = requests.get(api_url, auth=auth, timeout=HTTP_TIMEOUT_SECONDS)
50
+ resp.raise_for_status()
51
+ data = resp.json()
52
+ number = data.get("number")
53
+ building = data.get("building", False)
54
+ result = data.get("result")
55
+ url = data.get("url", "")
56
+ status = "IN_PROGRESS" if (building or result is None) else str(result).upper()
57
+ except requests.RequestException as exc:
58
+ logger.exception("Error getting build status for '%s'", job_name)
59
+ return f"Error getting build status: {exc}"
60
+ else:
61
+ return f"job={job_name} build={number} status={status} url={url}"
62
+
63
+
64
+ def get_console_log(job_name: str, build_number: int | None = None, start: int = 0) -> str:
65
+ """Retrieve the console log output for a Jenkins build.
66
+
67
+ Args:
68
+ job_name: Jenkins job name.
69
+ build_number: Specific build number; omit to use the latest build.
70
+ start: Byte offset to start reading from (0 for the full log).
71
+
72
+ Returns:
73
+ Log text as a string.
74
+ Returns an error string if Jenkins is not configured or the request fails.
75
+ """
76
+ config = get_jenkins_config()
77
+ if config is None:
78
+ return "Error: Jenkins is not configured (jenkins.yaml not found)"
79
+
80
+ base_url = config.base_url.rstrip("/")
81
+ auth = get_auth(config)
82
+ if build_number is None:
83
+ log_url = CONSOLE_LOG_LATEST_URL.format(base_url=base_url, job_name=job_name, start=start)
84
+ else:
85
+ log_url = CONSOLE_LOG_URL.format(
86
+ base_url=base_url, job_name=job_name, build_number=build_number, start=start
87
+ )
88
+
89
+ logger.info("Getting console log for job='%s' build=%s start=%s", job_name, build_number, start)
90
+ try:
91
+ resp = requests.get(log_url, auth=auth, timeout=HTTP_TIMEOUT_SECONDS)
92
+ resp.raise_for_status()
93
+ except requests.RequestException as exc:
94
+ logger.exception("Error getting console log for '%s'", job_name)
95
+ return f"Error getting console log: {exc}"
96
+ else:
97
+ return resp.text
@@ -9,6 +9,13 @@ from typing import TYPE_CHECKING, Any
9
9
 
10
10
  import requests
11
11
 
12
+ from autobots_devtools_shared_lib.common.config.jenkins_constants import (
13
+ API_JSON_SUFFIX,
14
+ BUILD_STATUS_URL,
15
+ HTTP_TIMEOUT_SECONDS,
16
+ JOB_URL_SEGMENT,
17
+ QUEUE_INITIAL_DELAY_SECONDS,
18
+ )
12
19
  from autobots_devtools_shared_lib.common.observability.logging_utils import get_logger
13
20
 
14
21
  if TYPE_CHECKING:
@@ -51,13 +58,13 @@ def poll_queue_for_build_number(
51
58
  logger.error(msg)
52
59
  return {"status": "error", "message": msg, "build_number": None, "build_url": None}
53
60
 
54
- time.sleep(1)
55
- queue_api_url = f"{queue_location}api/json"
61
+ time.sleep(QUEUE_INITIAL_DELAY_SECONDS)
62
+ queue_api_url = f"{queue_location}{API_JSON_SUFFIX}"
56
63
  logger.info(f"Polling Jenkins queue: {queue_api_url}")
57
64
 
58
65
  for attempt in range(polling.queue_max_retries):
59
66
  try:
60
- resp = requests.get(queue_api_url, auth=auth, timeout=30)
67
+ resp = requests.get(queue_api_url, auth=auth, timeout=HTTP_TIMEOUT_SECONDS)
61
68
  resp.raise_for_status()
62
69
  data = resp.json()
63
70
  executable = data.get("executable")
@@ -106,10 +113,12 @@ def wait_for_build(
106
113
  ) -> str:
107
114
  """Poll the build API until the result is non-null or the timeout is reached."""
108
115
  elapsed = 0
109
- api_url = f"{base_url}/job/{job_name}/{build_number}/api/json"
116
+ api_url = BUILD_STATUS_URL.format(
117
+ base_url=base_url, job_name=job_name, build_number=build_number
118
+ )
110
119
  while elapsed < polling.max_wait_seconds:
111
120
  try:
112
- resp = requests.get(api_url, auth=auth, timeout=30)
121
+ resp = requests.get(api_url, auth=auth, timeout=HTTP_TIMEOUT_SECONDS)
113
122
  resp.raise_for_status()
114
123
  data = resp.json()
115
124
  building = data.get("building", False)
@@ -148,7 +157,7 @@ def extract_job_name_from_url(url: str) -> str:
148
157
  """
149
158
  parts = [p for p in url.split("/") if p]
150
159
  try:
151
- job_idx = parts.index("job")
160
+ job_idx = parts.index(JOB_URL_SEGMENT)
152
161
  return parts[job_idx + 1]
153
162
  except (ValueError, IndexError):
154
163
  return url