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
3 changes: 1 addition & 2 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
> **You are reading the Github README!**
>
> - 📚 **Documentation**: See our [technical documentation](https://deepcritical.github.io/GradioDemo/) for detailed information
> - 📖 **Demo README**: Check out the [Demo README](..README.md) for for more information about our MCP Hackathon submission
> - 🏆 **Hackathon Submission**: Keep reading below for more information about our MCP Hackathon submission
> - 📖 **Demo README**: Check out the [Demo README](..README.md) for more information > - 🏆 **Demo**: Kindly consider using our [Free Demo](https://hf.co/DataQuests/GradioDemo)


<div align="center">
Expand Down
118 changes: 113 additions & 5 deletions .github/scripts/deploy_to_hf_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Set

Expand Down Expand Up @@ -38,6 +39,7 @@ def get_excluded_dirs() -> Set[str]:
"dist",
".eggs",
"htmlcov",
"hf_space", # Exclude the cloned HF Space directory itself
}


Expand All @@ -48,7 +50,6 @@ def get_excluded_files() -> Set[str]:
"mkdocs.yml",
"uv.lock",
"AGENTS.txt",
"CONTRIBUTING.md",
".env",
".env.local",
"*.local",
Expand Down Expand Up @@ -101,9 +102,22 @@ def deploy_to_hf_space() -> None:
hf_username = os.getenv("HF_USERNAME") # Can be username or organization name
space_name = os.getenv("HF_SPACE_NAME")

if not all([hf_token, hf_username, space_name]):
# Check which variables are missing and provide helpful error message
missing = []
if not hf_token:
missing.append("HF_TOKEN (should be in repository secrets)")
if not hf_username:
missing.append("HF_USERNAME (should be in repository variables)")
if not space_name:
missing.append("HF_SPACE_NAME (should be in repository variables)")

if missing:
raise ValueError(
"Missing required environment variables: HF_TOKEN, HF_USERNAME, HF_SPACE_NAME"
f"Missing required environment variables: {', '.join(missing)}\n"
f"Please configure:\n"
f" - HF_TOKEN in Settings > Secrets and variables > Actions > Secrets\n"
f" - HF_USERNAME in Settings > Secrets and variables > Actions > Variables\n"
f" - HF_SPACE_NAME in Settings > Secrets and variables > Actions > Variables"
)

# HF_USERNAME can be either a username or organization name
Expand Down Expand Up @@ -134,8 +148,36 @@ def deploy_to_hf_space() -> None:
)
print(f"✅ Created new Space: {repo_id}")

# Configure Git credential helper for authentication
# This is needed for Git LFS to work properly with fine-grained tokens
print("🔐 Configuring Git credentials...")

# Use Git credential store to store the token
# This allows Git LFS to authenticate properly
temp_dir = Path(tempfile.gettempdir())
credential_store = temp_dir / ".git-credentials-hf"

# Write credentials in the format: https://username:token@huggingface.co
credential_store.write_text(f"https://{hf_username}:{hf_token}@huggingface.co\n", encoding="utf-8")
try:
credential_store.chmod(0o600) # Secure permissions (Unix only)
except OSError:
# Windows doesn't support chmod, skip
pass

# Configure Git to use the credential store
subprocess.run(
["git", "config", "--global", "credential.helper", f"store --file={credential_store}"],
check=True,
capture_output=True,
)

# Also set environment variable for Git LFS
os.environ["GIT_CREDENTIAL_HELPER"] = f"store --file={credential_store}"

# Clone repository using git
space_url = f"https://{hf_token}@huggingface.co/spaces/{repo_id}"
# Use the token in the URL for initial clone, but LFS will use credential store
space_url = f"https://{hf_username}:{hf_token}@huggingface.co/spaces/{repo_id}"

if Path(local_dir).exists():
print(f"🧹 Removing existing {local_dir} directory...")
Expand All @@ -150,10 +192,58 @@ def deploy_to_hf_space() -> None:
text=True,
)
print(f"✅ Cloned Space repository")

# After clone, configure the remote to use credential helper
# This ensures future operations (like push) use the credential store
os.chdir(local_dir)
subprocess.run(
["git", "remote", "set-url", "origin", f"https://huggingface.co/spaces/{repo_id}"],
check=True,
capture_output=True,
)
os.chdir("..")

except subprocess.CalledProcessError as e:
error_msg = e.stderr if e.stderr else e.stdout if e.stdout else "Unknown error"
print(f"❌ Failed to clone Space repository: {error_msg}")
raise RuntimeError(f"Git clone failed: {error_msg}") from e

# Try alternative: clone with LFS skip, then fetch LFS files separately
print("🔄 Trying alternative clone method (skip LFS during clone)...")
try:
env = os.environ.copy()
env["GIT_LFS_SKIP_SMUDGE"] = "1" # Skip LFS during clone

subprocess.run(
["git", "clone", space_url, local_dir],
check=True,
capture_output=True,
text=True,
env=env,
)
print(f"✅ Cloned Space repository (LFS skipped)")

# Configure remote
os.chdir(local_dir)
subprocess.run(
["git", "remote", "set-url", "origin", f"https://huggingface.co/spaces/{repo_id}"],
check=True,
capture_output=True,
)

# Try to fetch LFS files with proper authentication
print("📥 Fetching LFS files...")
subprocess.run(
["git", "lfs", "pull"],
check=False, # Don't fail if LFS pull fails - we'll continue without LFS files
capture_output=True,
text=True,
)
os.chdir("..")
print(f"✅ Repository cloned (LFS files may be incomplete, but deployment can continue)")
except subprocess.CalledProcessError as e2:
error_msg2 = e2.stderr if e2.stderr else e2.stdout if e2.stdout else "Unknown error"
print(f"❌ Alternative clone method also failed: {error_msg2}")
raise RuntimeError(f"Git clone failed: {error_msg}") from e

# Get exclusion sets
excluded_dirs = get_excluded_dirs()
Expand All @@ -180,6 +270,10 @@ def deploy_to_hf_space() -> None:
if ".git" in item.parts:
continue

# Skip if in hf_space directory (the cloned Space directory)
if "hf_space" in item.parts:
continue

# Skip if should be excluded
if should_exclude(item, excluded_dirs, excluded_files):
continue
Expand Down Expand Up @@ -253,6 +347,12 @@ def deploy_to_hf_space() -> None:
capture_output=True,
)
print("📤 Pushing to Hugging Face Space...")
# Ensure remote URL uses credential helper (not token in URL)
subprocess.run(
["git", "remote", "set-url", "origin", f"https://huggingface.co/spaces/{repo_id}"],
check=True,
capture_output=True,
)
subprocess.run(
["git", "push"],
check=True,
Expand All @@ -273,6 +373,14 @@ def deploy_to_hf_space() -> None:
finally:
# Return to original directory
os.chdir(original_cwd)

# Clean up credential store for security
try:
if credential_store.exists():
credential_store.unlink()
except Exception:
# Ignore cleanup errors
pass

print(f"🎉 Successfully deployed to: https://huggingface.co/spaces/{repo_id}")

Expand Down
9 changes: 6 additions & 3 deletions .github/workflows/deploy-hf-space.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,18 @@ jobs:

- name: Deploy to Hugging Face Space
env:
# Token from secrets (sensitive data)
HF_TOKEN: ${{ secrets.HF_TOKEN }}
HF_USERNAME: ${{ secrets.HF_USERNAME }}
HF_SPACE_NAME: ${{ secrets.HF_SPACE_NAME }}
# Username/Organization from repository variables (non-sensitive)
HF_USERNAME: ${{ vars.HF_USERNAME }}
# Space name from repository variables (non-sensitive)
HF_SPACE_NAME: ${{ vars.HF_SPACE_NAME }}
run: |
python .github/scripts/deploy_to_hf_space.py

- name: Verify deployment
if: success()
run: |
echo "✅ Deployment completed successfully!"
echo "Space URL: https://huggingface.co/spaces/${{ secrets.HF_USERNAME }}/${{ secrets.HF_SPACE_NAME }}"
echo "Space URL: https://huggingface.co/spaces/${{ vars.HF_USERNAME }}/${{ vars.HF_SPACE_NAME }}"

9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ app_file: src/app.py
hf_oauth: true
hf_oauth_expiration_minutes: 480
hf_oauth_scopes:
- inference-api
# Required for HuggingFace Inference API (includes all third-party providers)
# This scope grants access to:
# - HuggingFace's own Inference API
# - Third-party inference providers (nebius, together, scaleway, hyperbolic, novita, nscale, sambanova, ovh, fireworks, etc.)
# - All models available through the Inference Providers API
- inference-api
# Optional: Uncomment if you need to access user's billing information
# - read-billing
pinned: true
license: mit
tags:
Expand Down
5 changes: 5 additions & 0 deletions dev/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@








5 changes: 5 additions & 0 deletions docs/LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.







11 changes: 10 additions & 1 deletion docs/api/orchestrators.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ Runs iterative research flow.
- `background_context`: Background context (default: "")
- `output_length`: Optional description of desired output length (default: "")
- `output_instructions`: Optional additional instructions for report generation (default: "")
- `message_history`: Optional user conversation history in Pydantic AI `ModelMessage` format (default: None)

**Returns**: Final report string.

**Note**: The `message_history` parameter enables multi-turn conversations by providing context from previous interactions.

**Note**: `max_iterations`, `max_time_minutes`, and `token_budget` are constructor parameters, not `run()` parameters.

## DeepResearchFlow
Expand All @@ -46,9 +49,12 @@ Runs deep research flow.

**Parameters**:
- `query`: Research query string
- `message_history`: Optional user conversation history in Pydantic AI `ModelMessage` format (default: None)

**Returns**: Final report string.

**Note**: The `message_history` parameter enables multi-turn conversations by providing context from previous interactions.

**Note**: `max_iterations_per_section`, `max_time_minutes`, and `token_budget` are constructor parameters, not `run()` parameters.

## GraphOrchestrator
Expand All @@ -69,10 +75,13 @@ Runs graph-based research orchestration.

**Parameters**:
- `query`: Research query string
- `message_history`: Optional user conversation history in Pydantic AI `ModelMessage` format (default: None)

**Yields**: `AgentEvent` objects during graph execution.

**Note**: `research_mode` and `use_graph` are constructor parameters, not `run()` parameters.
**Note**:
- `research_mode` and `use_graph` are constructor parameters, not `run()` parameters.
- The `message_history` parameter enables multi-turn conversations by providing context from previous interactions. Message history is stored in `GraphExecutionContext` and passed to agents during execution.

## Orchestrator Factory

Expand Down
38 changes: 38 additions & 0 deletions docs/architecture/graph_orchestration.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,44 @@

DeepCritical implements a graph-based orchestration system for research workflows using Pydantic AI agents as nodes. This enables better parallel execution, conditional routing, and state management compared to simple agent chains.

## Conversation History

DeepCritical supports multi-turn conversations through Pydantic AI's native message history format. The system maintains two types of history:

1. **User Conversation History**: Multi-turn user interactions (from Gradio chat interface) stored as `list[ModelMessage]`
2. **Research Iteration History**: Internal research process state (existing `Conversation` model)

### Message History Flow

```
Gradio Chat History → convert_gradio_to_message_history() → GraphOrchestrator.run(message_history)
GraphExecutionContext (stores message_history)
Agent Nodes (receive message_history via agent.run())
WorkflowState (persists user_message_history)
```

### Usage

Message history is automatically converted from Gradio format and passed through the orchestrator:

```python
# In app.py - automatic conversion
message_history = convert_gradio_to_message_history(history) if history else None
async for event in orchestrator.run(query, message_history=message_history):
yield event
```

Agents receive message history through their `run()` methods:

```python
# In agent execution
if message_history:
result = await agent.run(input_data, message_history=message_history)
```

## Graph Patterns

### Iterative Research Graph
Expand Down
16 changes: 13 additions & 3 deletions src/agents/knowledge_gap.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
import structlog
from pydantic_ai import Agent

try:
from pydantic_ai import ModelMessage
except ImportError:
ModelMessage = Any # type: ignore[assignment, misc]

from src.agent_factory.judges import get_model
from src.utils.exceptions import ConfigurationError
from src.utils.models import KnowledgeGapOutput
Expand Down Expand Up @@ -68,6 +73,7 @@ async def evaluate(
query: str,
background_context: str = "",
conversation_history: str = "",
message_history: list[ModelMessage] | None = None,
iteration: int = 0,
time_elapsed_minutes: float = 0.0,
max_time_minutes: int = 10,
Expand All @@ -78,7 +84,8 @@ async def evaluate(
Args:
query: The original research query
background_context: Optional background context
conversation_history: History of actions, findings, and thoughts
conversation_history: History of actions, findings, and thoughts (backward compat)
message_history: Optional user conversation history (Pydantic AI format)
iteration: Current iteration number
time_elapsed_minutes: Time elapsed so far
max_time_minutes: Maximum time allowed
Expand Down Expand Up @@ -111,8 +118,11 @@ async def evaluate(
"""

try:
# Run the agent
result = await self.agent.run(user_message)
# Run the agent with message_history if provided
if message_history:
result = await self.agent.run(user_message, message_history=message_history)
else:
result = await self.agent.run(user_message)
evaluation = result.output

self.logger.info(
Expand Down
Loading
Loading