Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ repos:
hooks:
- id: bandit
args: ["-c", "pyproject.toml"]
additional_dependencies: ["bandit[toml]"]
additional_dependencies: ["bandit[toml]", "pbr"]

# Markdown linting
- repo: https://github.com/igorshubovych/markdownlint-cli
Expand Down
111 changes: 55 additions & 56 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ dependencies = [
"loguru>=0.7.3,<0.8.0",
"mcp>=1.0.0",
"google-generativeai>=0.8.0,<1.0.0",
"python-dotenv>=1.0.0,<2.0.0"
"python-dotenv>=1.0.0,<2.0.0",
"dirtyjson (>=1.0.8,<2.0.0)",
"pre-commit (>=4.3.0,<5.0.0)"
]

[project.scripts]
Expand Down Expand Up @@ -132,3 +134,10 @@ inherit = false
match = "(?!test_).*\\.py"
match-dir = "(?!tests|migrations|__pycache__).*"
ignore = "D100,D104" # Ignore missing docstrings in modules and packages for now

[dependency-groups]
dev = [
"pre-commit (>=4.3.0,<5.0.0)",
"bandit (>=1.8.6,<2.0.0)",
"pbr (>=7.0.1,<8.0.0)"
]
34 changes: 19 additions & 15 deletions src/shardguard/core/planning.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import re
from typing import Protocol

import dirtyjson

from .llm_providers import LLMProviderFactory
from .mcp_integration import MCPClient

Expand Down Expand Up @@ -71,21 +73,23 @@ async def get_available_tools_description(self) -> str:

def _extract_json_from_response(self, response: str) -> str:
"""Extract JSON from LLM response that might contain extra text."""
# Try to find JSON block enclosed in curly braces
matches = re.findall(r"\{.*\}", response, re.DOTALL)

if matches:
# Return the longest JSON-like match
json_candidate = max(matches, key=len)
# Validate that it's actually valid JSON
try:
json.loads(json_candidate)
return json_candidate
except json.JSONDecodeError:
pass

# If no valid JSON found, return the original response
return response
try:
obj = dirtyjson.loads(response, search_for_first_object=True)
return json.dumps(obj)
except Exception:
# Try to find a JSON block enclosed in curly braces as a fallback
matches = re.findall(r"\{.*\}", response, re.DOTALL)
if matches:
# Return the longest JSON-like match
json_candidate = max(matches, key=len)
# Validate that it's actually valid JSON
try:
json.loads(json_candidate)
return json_candidate
except json.JSONDecodeError:
pass
# If no valid JSON is found, return the original response
return response

def _create_fallback_response(self, prompt: str, error: str) -> str:
"""Create a fallback response when plan generation fails."""
Expand Down