Skip to content
Open
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@ eggs/

# Jupyter Notebook checkpoints and modified notebooks
.ipynb_checkpoints/
05*_*.ipynb

# env
chemgraph-env/
# env
.env
# Log files
*run_logs/
cg_logs/
runs/
*vib/
plots/
initial_evaluations/
Expand All @@ -60,5 +63,12 @@ nwchem.nwo

vib*.traj

# Local calculation artifacts
pyscf_output*/
pyscf_h2*/

# Local persistent working memory
current_status.md

# Kubernetes secrets (keep secrets.yaml.template, ignore actual secrets)
k8s/secrets.yaml
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ ui = [
parsl = [
"parsl",
]
pyscf = [
"pyscf",
]
xanes = [
"mp-api; python_version >= '3.11'",
"parsl"
Expand Down
47 changes: 37 additions & 10 deletions src/chemgraph/cli/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
# Banner
# ---------------------------------------------------------------------------


def create_banner() -> Panel:
"""Create a welcome banner for ChemGraph CLI."""
banner_text = """
Expand All @@ -45,6 +46,7 @@ def create_banner() -> Panel:
# Model listing
# ---------------------------------------------------------------------------


def list_models() -> None:
"""Display available models in a formatted table."""
console.print(Panel("Available Models", style="bold cyan"))
Expand Down Expand Up @@ -89,6 +91,7 @@ def list_models() -> None:
# API-key status
# ---------------------------------------------------------------------------


def check_api_keys_status() -> None:
"""Display API key availability status."""
console.print(Panel("API Key Status", style="bold cyan"))
Expand Down Expand Up @@ -162,12 +165,38 @@ def check_api_keys_status() -> None:
# Response formatting
# ---------------------------------------------------------------------------

def _is_atomic_json(content: str) -> bool:

def _content_to_text(content: Any) -> str:
"""Extract displayable text from common LangChain/MCP content shapes."""
if isinstance(content, str):
return content

if isinstance(content, list):
parts: list[str] = []
for block in content:
if isinstance(block, dict):
text = block.get("text")
if isinstance(text, str):
parts.append(text)
else:
text = getattr(block, "text", None)
if isinstance(text, str):
parts.append(text)
return "\n".join(parts)

return ""


def _is_atomic_json(content: Any) -> bool:
"""Return True if *content* is a JSON string with atomic-structure keys.
This replaces the old fragile substring check (Bug 10) with a
proper parse attempt.
"""
content = _content_to_text(content)
if not content:
return False

try:
data = json.loads(content.strip())
except (json.JSONDecodeError, ValueError):
Expand Down Expand Up @@ -197,14 +226,14 @@ def format_response(result: Any, verbose: bool = False) -> None:
final_answer = ""
for message in reversed(messages):
if hasattr(message, "content") and hasattr(message, "type"):
if message.type == "ai" and message.content.strip():
content = message.content.strip()
content = _content_to_text(message.content).strip()
if message.type == "ai" and content:
if not _is_atomic_json(content):
final_answer = content
break
elif isinstance(message, dict):
if message.get("type") == "ai" and message.get("content", "").strip():
content = message["content"].strip()
content = _content_to_text(message.get("content", "")).strip()
if message.get("type") == "ai" and content:
if not _is_atomic_json(content):
final_answer = content
break
Expand All @@ -223,9 +252,9 @@ def format_response(result: Any, verbose: bool = False) -> None:
for message in messages:
content = ""
if hasattr(message, "content"):
content = message.content
content = _content_to_text(message.content)
elif isinstance(message, dict):
content = message.get("content", "")
content = _content_to_text(message.get("content", ""))

if content and _is_atomic_json(content):
console.print(
Expand All @@ -239,7 +268,5 @@ def format_response(result: Any, verbose: bool = False) -> None:
# Verbose output
if verbose:
console.print(
Panel(
f"Messages: {len(messages)}", title="Debug Information", style="dim"
)
Panel(f"Messages: {len(messages)}", title="Debug Information", style="dim")
)
Loading