talkops-opscloud 0.3.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. opscloud/__init__.py +5 -0
  2. opscloud/__main__.py +6 -0
  3. opscloud/_constants.py +53 -0
  4. opscloud/_debug.py +145 -0
  5. opscloud/_debug_buffer.py +250 -0
  6. opscloud/_textual_patches.py +367 -0
  7. opscloud/_version.py +15 -0
  8. opscloud/agent/__init__.py +27 -0
  9. opscloud/agent/config.py +168 -0
  10. opscloud/agent/factory.py +1103 -0
  11. opscloud/approval_mode.py +44 -0
  12. opscloud/backend/__init__.py +49 -0
  13. opscloud/backend/composite.py +89 -0
  14. opscloud/backend/local.py +183 -0
  15. opscloud/backend/registry.py +79 -0
  16. opscloud/backend/sandbox_config.py +107 -0
  17. opscloud/backend/sandbox_factory.py +432 -0
  18. opscloud/built_in_skills/remember/SKILL.md +124 -0
  19. opscloud/built_in_skills/skill-creator/SKILL.md +348 -0
  20. opscloud/built_in_skills/skill-creator/scripts/init_skill.py +371 -0
  21. opscloud/built_in_skills/skill-creator/scripts/quick_validate.py +165 -0
  22. opscloud/cli/__init__.py +1 -0
  23. opscloud/cli/commands/__init__.py +15 -0
  24. opscloud/cli/commands/auth.py +45 -0
  25. opscloud/cli/commands/doctor.py +100 -0
  26. opscloud/cli/commands/mcp.py +137 -0
  27. opscloud/cli/commands/threads.py +51 -0
  28. opscloud/cli/interactive.py +154 -0
  29. opscloud/cli/main.py +903 -0
  30. opscloud/cli/non_interactive.py +425 -0
  31. opscloud/cli/server_manager.py +58 -0
  32. opscloud/commands/__init__.py +19 -0
  33. opscloud/commands/_base.py +110 -0
  34. opscloud/commands/_guards.py +52 -0
  35. opscloud/commands/_router.py +146 -0
  36. opscloud/commands/_types.py +36 -0
  37. opscloud/commands/core/__init__.py +47 -0
  38. opscloud/commands/core/auth.py +94 -0
  39. opscloud/commands/core/bug.py +46 -0
  40. opscloud/commands/core/clear.py +106 -0
  41. opscloud/commands/core/cloud.py +185 -0
  42. opscloud/commands/core/compact.py +128 -0
  43. opscloud/commands/core/config_cmd.py +120 -0
  44. opscloud/commands/core/context.py +220 -0
  45. opscloud/commands/core/cost.py +44 -0
  46. opscloud/commands/core/doctor.py +535 -0
  47. opscloud/commands/core/effort.py +108 -0
  48. opscloud/commands/core/exit_cmd.py +41 -0
  49. opscloud/commands/core/fast.py +148 -0
  50. opscloud/commands/core/help_cmd.py +94 -0
  51. opscloud/commands/core/mcp.py +134 -0
  52. opscloud/commands/core/model.py +109 -0
  53. opscloud/commands/core/permissions.py +196 -0
  54. opscloud/commands/core/plugins.py +92 -0
  55. opscloud/commands/core/pool.py +108 -0
  56. opscloud/commands/core/resume.py +97 -0
  57. opscloud/commands/core/skills.py +178 -0
  58. opscloud/commands/power/__init__.py +51 -0
  59. opscloud/commands/power/agents.py +98 -0
  60. opscloud/commands/power/btw.py +85 -0
  61. opscloud/commands/power/copy.py +97 -0
  62. opscloud/commands/power/goal.py +548 -0
  63. opscloud/commands/power/loop.py +295 -0
  64. opscloud/commands/power/memory.py +287 -0
  65. opscloud/commands/power/review.py +152 -0
  66. opscloud/commands/power/rubric.py +287 -0
  67. opscloud/commands/power/runtime.py +321 -0
  68. opscloud/commands/power/skill_creator.py +70 -0
  69. opscloud/commands/power/skill_invoke.py +165 -0
  70. opscloud/commands/power/tasks.py +209 -0
  71. opscloud/commands/power/trace.py +117 -0
  72. opscloud/commands/power/ui_toggles.py +135 -0
  73. opscloud/commands/power/version.py +52 -0
  74. opscloud/commands/registry.py +176 -0
  75. opscloud/config/__init__.py +114 -0
  76. opscloud/config/adapters/__init__.py +5 -0
  77. opscloud/config/adapters/postgres.py +776 -0
  78. opscloud/config/adapters/sqlite.py +769 -0
  79. opscloud/config/aws.py +248 -0
  80. opscloud/config/cloud_profiles.py +437 -0
  81. opscloud/config/env_vars.py +81 -0
  82. opscloud/config/langsmith.py +319 -0
  83. opscloud/config/manifest.py +817 -0
  84. opscloud/config/metadata.py +205 -0
  85. opscloud/config/paths.py +604 -0
  86. opscloud/config/plugins.py +130 -0
  87. opscloud/config/settings.py +834 -0
  88. opscloud/config/store.py +453 -0
  89. opscloud/config/store_factory.py +97 -0
  90. opscloud/config/toml_config.py +562 -0
  91. opscloud/editor.py +173 -0
  92. opscloud/exceptions.py +65 -0
  93. opscloud/file_ops.py +217 -0
  94. opscloud/hooks/__init__.py +139 -0
  95. opscloud/hooks/env.py +44 -0
  96. opscloud/hooks/interrupt.py +121 -0
  97. opscloud/hooks/models/__init__.py +109 -0
  98. opscloud/hooks/models/adapters.py +19 -0
  99. opscloud/hooks/models/config.py +79 -0
  100. opscloud/hooks/models/domain.py +434 -0
  101. opscloud/hooks/models/transport.py +37 -0
  102. opscloud/hooks/tools.py +231 -0
  103. opscloud/input.py +48 -0
  104. opscloud/integrations/__init__.py +80 -0
  105. opscloud/integrations/base.py +153 -0
  106. opscloud/integrations/content_filter.py +119 -0
  107. opscloud/integrations/event_bus.py +480 -0
  108. opscloud/integrations/hooks.py +238 -0
  109. opscloud/integrations/identity.py +181 -0
  110. opscloud/integrations/notifications.py +134 -0
  111. opscloud/integrations/stream_bridge.py +397 -0
  112. opscloud/integrations/thread_store.py +81 -0
  113. opscloud/json_types.py +20 -0
  114. opscloud/mcp/__init__.py +57 -0
  115. opscloud/mcp/config.py +203 -0
  116. opscloud/mcp/discovery.py +229 -0
  117. opscloud/mcp/mcp_info.py +109 -0
  118. opscloud/mcp/middleware.py +102 -0
  119. opscloud/mcp/preload.py +505 -0
  120. opscloud/mcp/raw_config.py +149 -0
  121. opscloud/mcp/semantic_profiler.py +305 -0
  122. opscloud/mcp/session_manager.py +727 -0
  123. opscloud/mcp/trust.py +105 -0
  124. opscloud/memory/__init__.py +27 -0
  125. opscloud/memory/branch.py +61 -0
  126. opscloud/memory/guard.py +19 -0
  127. opscloud/memory/onboarding.py +120 -0
  128. opscloud/memory/registry.py +134 -0
  129. opscloud/memory/store.py +223 -0
  130. opscloud/middleware/__init__.py +106 -0
  131. opscloud/middleware/_repository_bounds.py +459 -0
  132. opscloud/middleware/ask_user.py +152 -0
  133. opscloud/middleware/auto_mode.py +60 -0
  134. opscloud/middleware/auto_mode_hitl.py +2434 -0
  135. opscloud/middleware/compaction.py +566 -0
  136. opscloud/middleware/configurable_model.py +233 -0
  137. opscloud/middleware/cost_tracking.py +1021 -0
  138. opscloud/middleware/glm_stall_recovery.py +109 -0
  139. opscloud/middleware/goal_criteria.py +1234 -0
  140. opscloud/middleware/goal_state_notice.py +712 -0
  141. opscloud/middleware/goal_tools.py +407 -0
  142. opscloud/middleware/headless_mcp_guard.py +88 -0
  143. opscloud/middleware/jev_model_router.py +1018 -0
  144. opscloud/middleware/local_context.py +460 -0
  145. opscloud/middleware/mcp_context.py +151 -0
  146. opscloud/middleware/mcp_middleware.py +146 -0
  147. opscloud/middleware/memory_guard.py +393 -0
  148. opscloud/middleware/model_retry.py +205 -0
  149. opscloud/middleware/registry.py +137 -0
  150. opscloud/middleware/reliable_rubric.py +709 -0
  151. opscloud/middleware/resume_state.py +91 -0
  152. opscloud/middleware/server_hooks.py +1189 -0
  153. opscloud/middleware/shell_allow_list.py +90 -0
  154. opscloud/middleware/skills.py +382 -0
  155. opscloud/middleware/subagent_artifacts.py +84 -0
  156. opscloud/middleware/subagent_telemetry.py +261 -0
  157. opscloud/middleware/subagents.py +243 -0
  158. opscloud/middleware/tool_filter.py +384 -0
  159. opscloud/middleware/unified_system_message.py +62 -0
  160. opscloud/model/__init__.py +37 -0
  161. opscloud/model/config.py +2992 -0
  162. opscloud/model/factory.py +477 -0
  163. opscloud/model/pool.py +459 -0
  164. opscloud/model/reasoning.py +282 -0
  165. opscloud/offload.py +173 -0
  166. opscloud/output.py +64 -0
  167. opscloud/plugins/__init__.py +189 -0
  168. opscloud/plugins/_json.py +22 -0
  169. opscloud/plugins/adapters/__init__.py +13 -0
  170. opscloud/plugins/adapters/agents.py +112 -0
  171. opscloud/plugins/adapters/commands.py +190 -0
  172. opscloud/plugins/adapters/mcp.py +363 -0
  173. opscloud/plugins/adapters/skills.py +132 -0
  174. opscloud/plugins/commands_cli.py +541 -0
  175. opscloud/plugins/discovery.py +512 -0
  176. opscloud/plugins/manifest.py +335 -0
  177. opscloud/plugins/marketplace.py +893 -0
  178. opscloud/plugins/models.py +334 -0
  179. opscloud/plugins/project_plugins.py +317 -0
  180. opscloud/plugins/protocol.py +31 -0
  181. opscloud/plugins/store.py +905 -0
  182. opscloud/plugins/substitution.py +122 -0
  183. opscloud/project_utils.py +145 -0
  184. opscloud/prompts/__init__.py +37 -0
  185. opscloud/prompts/system.py +483 -0
  186. opscloud/prompts/templates/system_prompt.md +166 -0
  187. opscloud/prompts/types.py +54 -0
  188. opscloud/rubrics/__init__.py +42 -0
  189. opscloud/rubrics/evaluator.py +404 -0
  190. opscloud/rubrics/evidence_extractor.py +229 -0
  191. opscloud/rubrics/generator.py +239 -0
  192. opscloud/rubrics/jev_compiler.py +175 -0
  193. opscloud/rubrics/jev_grader.py +297 -0
  194. opscloud/rubrics/middleware.py +8 -0
  195. opscloud/schema/__init__.py +107 -0
  196. opscloud/schema/interrupts.py +981 -0
  197. opscloud/security/__init__.py +91 -0
  198. opscloud/security/approval_mode.py +528 -0
  199. opscloud/security/approval_mode_source.py +230 -0
  200. opscloud/security/cli_ast_evaluator.py +552 -0
  201. opscloud/security/jev_classifier.py +651 -0
  202. opscloud/security/shell_safety.py +340 -0
  203. opscloud/security/unicode_security.py +439 -0
  204. opscloud/security/url_validation.py +160 -0
  205. opscloud/server/__init__.py +13 -0
  206. opscloud/server/_server_config.py +158 -0
  207. opscloud/server/server.py +242 -0
  208. opscloud/server/server_graph.py +93 -0
  209. opscloud/skills/__init__.py +91 -0
  210. opscloud/skills/commands.py +1099 -0
  211. opscloud/skills/invocation.py +200 -0
  212. opscloud/skills/load.py +388 -0
  213. opscloud/skills/loader.py +23 -0
  214. opscloud/skills/merge.py +65 -0
  215. opscloud/skills/registry.py +263 -0
  216. opscloud/skills/sources.py +174 -0
  217. opscloud/skills/trust.py +417 -0
  218. opscloud/state/__init__.py +142 -0
  219. opscloud/state/base.py +154 -0
  220. opscloud/state/cloud_context.py +262 -0
  221. opscloud/state/goal_channels.py +124 -0
  222. opscloud/state/goal_state_limits.py +175 -0
  223. opscloud/state/resume_state.py +58 -0
  224. opscloud/state/service_context.py +281 -0
  225. opscloud/state/session.py +957 -0
  226. opscloud/state/state_migration.py +86 -0
  227. opscloud/subagents/__init__.py +30 -0
  228. opscloud/subagents/loader.py +331 -0
  229. opscloud/subagents/subagents_parser.py +117 -0
  230. opscloud/subagents/types.py +25 -0
  231. opscloud/tools/__init__.py +36 -0
  232. opscloud/tools/catalog.py +37 -0
  233. opscloud/tools/display.py +24 -0
  234. opscloud/tools/fetch_url.py +176 -0
  235. opscloud/tools/goal_tools.py +423 -0
  236. opscloud/tools/registry.py +105 -0
  237. opscloud/tools/thread.py +32 -0
  238. opscloud/tools/web_search.py +215 -0
  239. opscloud/ui/__init__.py +119 -0
  240. opscloud/ui/app.py +5400 -0
  241. opscloud/ui/app.tcss +479 -0
  242. opscloud/ui/clipboard.py +147 -0
  243. opscloud/ui/command_registry.py +482 -0
  244. opscloud/ui/message_store.py +152 -0
  245. opscloud/ui/permission_store.py +360 -0
  246. opscloud/ui/preferences.py +113 -0
  247. opscloud/ui/prompt_manager.py +96 -0
  248. opscloud/ui/remote_client.py +505 -0
  249. opscloud/ui/textual_adapter.py +1269 -0
  250. opscloud/ui/theme.py +728 -0
  251. opscloud/ui/ui_help.py +415 -0
  252. opscloud/ui/widgets/__init__.py +383 -0
  253. opscloud/ui/widgets/_ask_user_types.py +81 -0
  254. opscloud/ui/widgets/_copy_spans.py +52 -0
  255. opscloud/ui/widgets/_inline_prompt.py +290 -0
  256. opscloud/ui/widgets/_js_eval_display.py +139 -0
  257. opscloud/ui/widgets/_links.py +175 -0
  258. opscloud/ui/widgets/_tool_stream.py +308 -0
  259. opscloud/ui/widgets/agent_selector.py +297 -0
  260. opscloud/ui/widgets/approval.py +741 -0
  261. opscloud/ui/widgets/ask_user.py +539 -0
  262. opscloud/ui/widgets/auth.py +695 -0
  263. opscloud/ui/widgets/auth_manager.py +214 -0
  264. opscloud/ui/widgets/auto_mode_notice.py +146 -0
  265. opscloud/ui/widgets/autocomplete.py +709 -0
  266. opscloud/ui/widgets/chat_input.py +805 -0
  267. opscloud/ui/widgets/cloud_selector.py +528 -0
  268. opscloud/ui/widgets/config_manager.py +652 -0
  269. opscloud/ui/widgets/debug_console.py +1269 -0
  270. opscloud/ui/widgets/devops_renderers.py +184 -0
  271. opscloud/ui/widgets/diff.py +237 -0
  272. opscloud/ui/widgets/effort_selector.py +166 -0
  273. opscloud/ui/widgets/goal_review.py +612 -0
  274. opscloud/ui/widgets/goal_status.py +57 -0
  275. opscloud/ui/widgets/infra_panel.py +141 -0
  276. opscloud/ui/widgets/install_confirm.py +114 -0
  277. opscloud/ui/widgets/loading.py +170 -0
  278. opscloud/ui/widgets/mcp_viewer.py +1204 -0
  279. opscloud/ui/widgets/messages.py +4461 -0
  280. opscloud/ui/widgets/model_selector.py +863 -0
  281. opscloud/ui/widgets/notification_center.py +108 -0
  282. opscloud/ui/widgets/notification_settings.py +119 -0
  283. opscloud/ui/widgets/operation_card.py +106 -0
  284. opscloud/ui/widgets/permissions_manager.py +667 -0
  285. opscloud/ui/widgets/plugin_manager.py +1212 -0
  286. opscloud/ui/widgets/pool_selector.py +376 -0
  287. opscloud/ui/widgets/preamble.py +53 -0
  288. opscloud/ui/widgets/skills_viewer.py +432 -0
  289. opscloud/ui/widgets/status.py +1037 -0
  290. opscloud/ui/widgets/subagent_panel.py +927 -0
  291. opscloud/ui/widgets/theme_selector.py +232 -0
  292. opscloud/ui/widgets/thread_selector.py +358 -0
  293. opscloud/ui/widgets/toast.py +31 -0
  294. opscloud/ui/widgets/tool_display.py +325 -0
  295. opscloud/ui/widgets/tool_renderers.py +234 -0
  296. opscloud/ui/widgets/tool_widgets.py +278 -0
  297. opscloud/ui/widgets/welcome.py +421 -0
  298. opscloud/ui/widgets/welcome_popup.py +102 -0
  299. opscloud/utils/__init__.py +24 -0
  300. opscloud/utils/cost_estimation.py +169 -0
  301. opscloud/utils/git.py +390 -0
  302. opscloud/utils/logger.py +571 -0
  303. opscloud/utils/session_stats.py +115 -0
  304. opscloud/utils/startup_error.py +37 -0
  305. talkops_opscloud-0.3.0b1.dist-info/METADATA +449 -0
  306. talkops_opscloud-0.3.0b1.dist-info/RECORD +309 -0
  307. talkops_opscloud-0.3.0b1.dist-info/WHEEL +4 -0
  308. talkops_opscloud-0.3.0b1.dist-info/entry_points.txt +2 -0
  309. talkops_opscloud-0.3.0b1.dist-info/licenses/LICENSE +175 -0
@@ -0,0 +1,54 @@
1
+ """Types and enums for OpsCloud prompts module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import StrEnum
6
+ from typing import Literal
7
+
8
+ from opscloud.utils.logger import get_logger
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ class CloudProvider(StrEnum):
14
+ """Supported cloud providers for OpsCloud operations and prompts."""
15
+
16
+ AWS = "aws"
17
+ AZURE = "azure"
18
+ GCP = "gcp"
19
+ MULTI = "multi"
20
+
21
+
22
+ FsToolName = Literal[
23
+ "read_file",
24
+ "write_file",
25
+ "edit_file",
26
+ "list_dir",
27
+ "grep_search",
28
+ "find_by_name",
29
+ ]
30
+
31
+
32
+ def normalize_cloud_provider(val: str | CloudProvider | None) -> CloudProvider:
33
+ """Normalize a cloud provider string or enum into a validated CloudProvider.
34
+
35
+ Defaults to `CloudProvider.AWS` when unspecified.
36
+ """
37
+ if val is None:
38
+ return CloudProvider.AWS
39
+
40
+ if isinstance(val, CloudProvider):
41
+ return val
42
+
43
+ normalized = val.strip().lower()
44
+ if normalized in {"aws", "amazon", "amazon_aws"}:
45
+ return CloudProvider.AWS
46
+ if normalized in {"azure", "az", "microsoft", "azure_cloud"}:
47
+ return CloudProvider.AZURE
48
+ if normalized in {"gcp", "google", "google_cloud"}:
49
+ return CloudProvider.GCP
50
+ if normalized in {"multi", "multi_cloud", "multicloud", "hybrid", "cross"}:
51
+ return CloudProvider.MULTI
52
+
53
+ logger.warning("Unrecognized cloud provider '%s'; defaulting to AWS.", val)
54
+ return CloudProvider.AWS
@@ -0,0 +1,42 @@
1
+ """Rubrics and evaluation package for opscloud."""
2
+
3
+ from opscloud.rubrics.evaluator import (
4
+ _RUBRIC_GRADER_SYSTEM_PROMPT,
5
+ _create_rubric_grader_tools,
6
+ _rubric_grader_system_prompt,
7
+ create_rubric_grader_agent,
8
+ evaluate_rubric,
9
+ )
10
+ from opscloud.rubrics.evidence_extractor import extract_grading_evidence
11
+ from opscloud.rubrics.generator import (
12
+ GOAL_AMENDMENT_SYSTEM_PROMPT,
13
+ GOAL_RUBRIC_SYSTEM_PROMPT,
14
+ generate_rubric,
15
+ _goal_amendment_human_prompt,
16
+ _goal_rubric_human_prompt,
17
+ )
18
+ from opscloud.rubrics.jev_compiler import CompiledRubric, JevCriteriaCompiler
19
+ from opscloud.rubrics.jev_grader import (
20
+ BLOCKER_DISPATCH,
21
+ JevHybridRubricGrader,
22
+ build_criteria_evaluations,
23
+ )
24
+
25
+ __all__ = [
26
+ "BLOCKER_DISPATCH",
27
+ "CompiledRubric",
28
+ "GOAL_AMENDMENT_SYSTEM_PROMPT",
29
+ "GOAL_RUBRIC_SYSTEM_PROMPT",
30
+ "JevCriteriaCompiler",
31
+ "JevHybridRubricGrader",
32
+ "_RUBRIC_GRADER_SYSTEM_PROMPT",
33
+ "_create_rubric_grader_tools",
34
+ "_goal_amendment_human_prompt",
35
+ "_goal_rubric_human_prompt",
36
+ "_rubric_grader_system_prompt",
37
+ "build_criteria_evaluations",
38
+ "create_rubric_grader_agent",
39
+ "evaluate_rubric",
40
+ "extract_grading_evidence",
41
+ "generate_rubric",
42
+ ]
@@ -0,0 +1,404 @@
1
+ """Grader tools, prompts, and standalone evaluator for rubric verification.
2
+
3
+ Provides:
4
+ - Isolated read-only tools for inspecting both offloaded outputs and repository files.
5
+ - Dynamic grader system prompt builder with domain-aware guidance for AWS Cloud & DevOps.
6
+ - ``create_rubric_grader_agent`` to instantiate the Rubric Grader as an individual agent.
7
+ - ``evaluate_rubric`` for direct, programmatic quality grading.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import inspect
13
+ from pathlib import Path, PurePosixPath
14
+ from typing import TYPE_CHECKING, Any, Callable, Sequence
15
+
16
+ from deepagents.middleware import GRADER_SYSTEM_PROMPT
17
+ from deepagents.middleware.rubric import (
18
+ RUBRIC_GRADER_MESSAGE_SOURCE,
19
+ CriterionEval,
20
+ CriterionFail,
21
+ CriterionPass,
22
+ GraderResponse,
23
+ GraderVerdict,
24
+ )
25
+ from langchain_core.messages import HumanMessage
26
+ from langchain_core.tools import BaseTool, StructuredTool, tool
27
+
28
+ from opscloud.middleware._repository_bounds import (
29
+ RepositoryBounds,
30
+ )
31
+ from opscloud.middleware.reliable_rubric import RubricGraderState
32
+ from opscloud.offload import _artifacts_root
33
+ from opscloud.utils.logger import get_logger
34
+
35
+ if TYPE_CHECKING:
36
+ from deepagents.backends.protocol import BackendProtocol
37
+ from langchain.agents.middleware.types import AgentMiddleware
38
+ from langchain_core.language_models import BaseChatModel
39
+
40
+ logger = get_logger(__name__)
41
+
42
+ _RUBRIC_GRADER_READ_FILE_PREFIX = "/large_tool_results/"
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Grader System Prompt
47
+ # ---------------------------------------------------------------------------
48
+
49
+
50
+ def _rubric_grader_system_prompt(
51
+ read_file_prefix: str = _RUBRIC_GRADER_READ_FILE_PREFIX,
52
+ repository_root: str | None = None,
53
+ context_tool_names: Sequence[str] | None = None,
54
+ repository_tool_names: Sequence[str] | None = None,
55
+ ) -> str:
56
+ """Build the comprehensive grader system prompt for AWS Cloud & DevOps verification."""
57
+ prompt = (
58
+ f"{GRADER_SYSTEM_PROMPT}\n\n"
59
+ "# OpsCloud — Independent Rubric Verification Grader\n\n"
60
+ "You are the independent Rubric Grader for OpsCloud. Your sole responsibility is to evaluate "
61
+ "whether the agent's work satisfies the accepted acceptance criteria without self-evaluation bias.\n\n"
62
+ "## Evidence Inspection Rules\n"
63
+ f"1. When the conversation transcript references an offloaded tool output saved under `{read_file_prefix}` "
64
+ "or an artifacts directory, use the `read_file` tool to inspect the referenced evidence before concluding "
65
+ "that a criterion lacks support.\n"
66
+ "2. Only mark a criterion satisfied if direct, verifiable evidence in the transcript, command outputs, or "
67
+ "on-disk files demonstrates it. Never assume or extrapolate completion.\n"
68
+ "3. If any criterion is not satisfied or is partially completed, provide clear, actionable feedback in the "
69
+ "deficiency report explaining exactly what is missing or failed."
70
+ )
71
+
72
+ if repository_root and repository_tool_names:
73
+ tools_desc = ", ".join(f"`{t}`" for t in repository_tool_names)
74
+ prompt += (
75
+ f"\n\n## Working Directory Inspection\n"
76
+ f"You have read-only tools ({tools_desc}) scoped to the working directory rooted at `{repository_root}`.\n"
77
+ "The transcript can truncate long command outputs; prefer directly inspecting modified or created files "
78
+ "(e.g. Terraform configs, CloudFormation templates, Dockerfiles, Kubernetes manifests, scripts, or tests) "
79
+ "to verify the criteria on disk. All repository inspection is read-only and confined to the repository root."
80
+ )
81
+
82
+ if context_tool_names:
83
+ c_names = ", ".join(f"`{t}`" for t in context_tool_names)
84
+ prompt += (
85
+ f"\n\n## External Context Tools\n"
86
+ f"Read-only external context tools are available: {c_names}. Use them when a criterion requires "
87
+ "verifying live cloud resources or external states."
88
+ )
89
+
90
+ return prompt
91
+
92
+
93
+ # Backward-compatible static system prompt
94
+ _RUBRIC_GRADER_SYSTEM_PROMPT = _rubric_grader_system_prompt()
95
+
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # Path Safety Validation
99
+ # ---------------------------------------------------------------------------
100
+
101
+
102
+ def _validate_rubric_grader_read_path(
103
+ file_path: str,
104
+ allowed_prefix: str | None = None,
105
+ repository_root: str | None = None,
106
+ ) -> str | None:
107
+ """Validate that a requested file path does not escape allowed boundaries."""
108
+ normalized = file_path.replace("\\", "/")
109
+ parts = PurePosixPath(normalized).parts
110
+ if ".." in parts or "~" in parts:
111
+ return "Invalid path: traversal characters forbidden."
112
+
113
+ # Check allowed prefix (e.g. /large_tool_results/ or offload directory)
114
+ if allowed_prefix and normalized.startswith(allowed_prefix):
115
+ return None
116
+ if normalized.startswith(_RUBRIC_GRADER_READ_FILE_PREFIX):
117
+ return None
118
+
119
+ # Check offload artifact root
120
+ try:
121
+ art_root = _artifacts_root().root.replace("\\", "/")
122
+ if normalized.startswith(art_root):
123
+ return None
124
+ except Exception:
125
+ pass
126
+
127
+ # Check repository root
128
+ if repository_root:
129
+ repo_norm = str(Path(repository_root).resolve()).replace("\\", "/")
130
+ try:
131
+ target_norm = str(Path(file_path).resolve()).replace("\\", "/")
132
+ if target_norm.startswith(repo_norm):
133
+ return None
134
+ except Exception:
135
+ pass
136
+
137
+ return f"Rubric grader cannot read path outside authorized directories: {file_path}"
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # Grader Tools Creation
142
+ # ---------------------------------------------------------------------------
143
+
144
+
145
+ def _create_rubric_grader_tools(
146
+ backend: Any = None,
147
+ *,
148
+ repository_backend: BackendProtocol | None = None,
149
+ repository_root: str | None = None,
150
+ context_tools: Sequence[BaseTool | Callable[..., Any]] = (),
151
+ ) -> list[BaseTool]:
152
+ """Create isolated read-only tools for rubric grading."""
153
+ if repository_root:
154
+ effective_root = repository_root
155
+ elif repository_backend is not None:
156
+ effective_root = "/"
157
+ else:
158
+ try:
159
+ from opscloud.project_utils import get_server_project_context
160
+
161
+ ctx = get_server_project_context()
162
+ effective_root = str(ctx.user_cwd) if ctx is not None else "/"
163
+ except Exception:
164
+ effective_root = "/"
165
+ read_file_prefix = _RUBRIC_GRADER_READ_FILE_PREFIX
166
+ try:
167
+ read_file_prefix = _artifacts_root().root
168
+ except Exception:
169
+ pass
170
+
171
+ bounds: RepositoryBounds | None = None
172
+ if repository_backend is not None and repository_root is not None:
173
+ try:
174
+ bounds = RepositoryBounds(repository_backend, root=repository_root)
175
+ except Exception as e:
176
+ logger.warning("Could not initialize RepositoryBounds for grader: %s", e)
177
+
178
+ @tool
179
+ def read_file(file_path: str, offset: int = 0, limit: int = 120) -> str:
180
+ """Read an offloaded tool result or a working-directory file.
181
+
182
+ Args:
183
+ file_path: Absolute or workspace path to the file.
184
+ offset: Starting line index (0-based).
185
+ limit: Maximum number of lines to return.
186
+ """
187
+ if err := _validate_rubric_grader_read_path(file_path, read_file_prefix, effective_root):
188
+ return err
189
+
190
+ try:
191
+ p = Path(file_path).expanduser().resolve()
192
+ if not p.exists():
193
+ return f"File {file_path} not found."
194
+ if not p.is_file():
195
+ return f"Path {file_path} is not a file."
196
+ content = p.read_text(encoding="utf-8", errors="replace")
197
+ lines = content.splitlines()
198
+ sliced = lines[offset : offset + limit]
199
+ return "\n".join(sliced)
200
+ except Exception as e:
201
+ return f"Error reading file {file_path}: {e}"
202
+
203
+ tools: list[BaseTool] = [read_file]
204
+
205
+ root_path = Path(effective_root).resolve()
206
+
207
+ def _resolve_target(rel_or_abs: str) -> Path:
208
+ p = Path(rel_or_abs)
209
+ if p.is_absolute():
210
+ return p.resolve()
211
+ return (root_path / p).resolve()
212
+
213
+ def _is_safe(target_p: Path) -> bool:
214
+ if bounds:
215
+ return bounds.safe_path(str(target_p))
216
+ try:
217
+ target_p.relative_to(root_path)
218
+ return True
219
+ except ValueError:
220
+ return False
221
+
222
+ # Add repository inspection tools if repository_backend or local workspace is bound
223
+ @tool
224
+ def ls(path: str = ".") -> str:
225
+ """List files in the working directory (read-only)."""
226
+ target = _resolve_target(path)
227
+ if not _is_safe(target):
228
+ return "Path is outside the repository bounds."
229
+ if not target.exists():
230
+ return f"Directory does not exist: {path}"
231
+ try:
232
+ entries = sorted(target.iterdir(), key=lambda p: (not p.is_dir(), p.name))
233
+ return "\n".join(f"{'[DIR] ' if e.is_dir() else '[FILE] '}{e.name}" for e in entries[:200])
234
+ except Exception as e:
235
+ return f"Error listing directory: {e}"
236
+
237
+ @tool
238
+ def glob(pattern: str, path: str = ".") -> str:
239
+ """Search for files matching pattern in the repository (read-only)."""
240
+ target = _resolve_target(path)
241
+ if not _is_safe(target):
242
+ return "Path is outside the repository bounds."
243
+ if not target.exists():
244
+ return f"Path does not exist: {path}"
245
+ try:
246
+ matches = [str(p.relative_to(target)) for p in list(target.glob(pattern))[:200]]
247
+ return "\n".join(matches) if matches else "No matching files found."
248
+ except Exception as e:
249
+ return f"Error searching glob: {e}"
250
+
251
+ @tool
252
+ def grep(pattern: str, path: str = ".") -> str:
253
+ """Search for text pattern in repository files (read-only)."""
254
+ target = _resolve_target(path)
255
+ if not _is_safe(target):
256
+ return "Path is outside the repository bounds."
257
+ if not target.exists():
258
+ return f"Path does not exist: {path}"
259
+ try:
260
+ results: list[str] = []
261
+ for file_path in target.rglob("*"):
262
+ if file_path.is_file() and not file_path.name.startswith("."):
263
+ try:
264
+ content = file_path.read_text(encoding="utf-8", errors="ignore")
265
+ for idx, line in enumerate(content.splitlines(), start=1):
266
+ if pattern in line:
267
+ rel = file_path.relative_to(target)
268
+ results.append(f"{rel}:{idx}: {line.strip()[:160]}")
269
+ if len(results) >= 100:
270
+ break
271
+ except Exception:
272
+ continue
273
+ if len(results) >= 100:
274
+ break
275
+ return "\n".join(results) if results else "Pattern not found."
276
+ except Exception as e:
277
+ return f"Error running grep: {e}"
278
+
279
+ tools.extend([ls, glob, grep])
280
+
281
+ # Append any valid context tools
282
+ for candidate in context_tools:
283
+ if isinstance(candidate, BaseTool):
284
+ tools.append(candidate)
285
+ elif inspect.iscoroutinefunction(candidate):
286
+ tools.append(StructuredTool.from_function(coroutine=candidate))
287
+ elif callable(candidate):
288
+ tools.append(StructuredTool.from_function(func=candidate))
289
+
290
+ return tools
291
+
292
+
293
+ # ---------------------------------------------------------------------------
294
+ # Standalone Rubric Grader Agent Factory & Evaluator
295
+ # ---------------------------------------------------------------------------
296
+
297
+
298
+ def create_rubric_grader_agent(
299
+ *,
300
+ model: str | BaseChatModel | None = None,
301
+ tools: Sequence[BaseTool] | None = None,
302
+ middleware: Sequence[AgentMiddleware[Any, Any]] | None = None,
303
+ context_schema: type[Any] | None = None,
304
+ system_prompt: str | None = None,
305
+ ) -> Any:
306
+ """Create an isolated Rubric Grader Agent as an individual agent graph."""
307
+ from deepagents._models import resolve_model
308
+ from langchain.agents import create_agent
309
+
310
+ from opscloud.model.factory import create_model as opscloud_create_model
311
+
312
+ if model is None:
313
+ model_res = opscloud_create_model()
314
+ chat_model = model_res.model
315
+ elif isinstance(model, str):
316
+ chat_model = resolve_model(model)
317
+ else:
318
+ chat_model = model
319
+
320
+ grader_tools = list(tools) if tools is not None else _create_rubric_grader_tools()
321
+ prompt = system_prompt or _RUBRIC_GRADER_SYSTEM_PROMPT
322
+
323
+ return create_agent(
324
+ model=chat_model,
325
+ system_prompt=prompt,
326
+ tools=grader_tools,
327
+ middleware=list(middleware or ()),
328
+ name=RUBRIC_GRADER_MESSAGE_SOURCE,
329
+ response_format=GraderResponse,
330
+ state_schema=RubricGraderState,
331
+ context_schema=context_schema,
332
+ )
333
+
334
+
335
+ def evaluate_rubric(
336
+ criteria: str,
337
+ evidence: str | Sequence[Any],
338
+ *,
339
+ model: str | BaseChatModel | None = None,
340
+ repository_root: str | None = None,
341
+ use_jev: bool | None = None,
342
+ ) -> GraderResponse:
343
+ """Directly evaluate criteria against evidence using the Rubric Grader Agent or Jev."""
344
+ from langchain_core.messages import BaseMessage
345
+
346
+ if use_jev is True:
347
+ from opscloud.rubrics.jev_compiler import JevCriteriaCompiler
348
+ from opscloud.rubrics.jev_grader import JevHybridRubricGrader
349
+
350
+ jev_grader = JevHybridRubricGrader()
351
+ if jev_grader.is_available():
352
+ compiled = JevCriteriaCompiler.compile("Direct Evaluation", criteria)
353
+ return jev_grader.grade(
354
+ compiled,
355
+ evidence=evidence,
356
+ fallback_llm_fn=lambda failing: evaluate_rubric(
357
+ criteria, evidence, model=model, repository_root=repository_root, use_jev=False
358
+ ),
359
+ )
360
+
361
+ tools = _create_rubric_grader_tools(repository_root=repository_root)
362
+ grader = create_rubric_grader_agent(model=model, tools=tools)
363
+
364
+ if isinstance(evidence, str):
365
+ content = f"<criteria>\n{criteria}\n</criteria>\n\n<transcript>\n{evidence}\n</transcript>"
366
+ elif isinstance(evidence, Sequence):
367
+ rendered = []
368
+ for msg in evidence:
369
+ if isinstance(msg, BaseMessage):
370
+ rendered.append(f"{msg.type}: {msg.content}")
371
+ else:
372
+ rendered.append(str(msg))
373
+ joined = "\n\n".join(rendered)
374
+ content = f"<criteria>\n{criteria}\n</criteria>\n\n<transcript>\n{joined}\n</transcript>"
375
+ else:
376
+ content = f"<criteria>\n{criteria}\n</criteria>\n\n<evidence>\n{evidence}\n</evidence>"
377
+
378
+ input_payload = {
379
+ "messages": [HumanMessage(content=content)],
380
+ "rubric_grading_operation_id": "direct_eval",
381
+ }
382
+ result = grader.invoke(input_payload)
383
+ if isinstance(result, GraderResponse):
384
+ return result
385
+ if isinstance(result, dict) and "structured_response" in result:
386
+ structured = result["structured_response"]
387
+ if isinstance(structured, GraderResponse):
388
+ return structured
389
+ if isinstance(structured, dict):
390
+ return GraderResponse.model_validate(structured)
391
+
392
+ verdict: GraderVerdict = "satisfied" if "satisfied" in str(result).lower() else "needs_revision"
393
+ explanation = str(result)
394
+ criteria_evals: list[CriterionEval]
395
+ if verdict == "satisfied":
396
+ criteria_evals = [CriterionPass(name="criteria_evaluation", passed=True)]
397
+ else:
398
+ criteria_evals = [CriterionFail(name="criteria_evaluation", passed=False, gap=explanation)]
399
+
400
+ return GraderResponse(
401
+ result=verdict,
402
+ explanation=explanation,
403
+ criteria=criteria_evals,
404
+ )