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
28 changes: 28 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# ignored files
.env
__pycache__/

# === Logs ===
*.log

# === Runtime artifacts ===
run/

# === Python cache ===
__pycache__/
*.py[cod] # for compiled files

# === Environment variables ===
.env
.env.local

# === Virtual environments (optional) ===
venv/
env/
.venv/

# === IDE / editor files (optional but common) ===
.idea/
.vscode/
*.swp
*.swo
47 changes: 47 additions & 0 deletions PROPOSAL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Artifact Management Proposal

## Current State
- **Logs:** `agent.log` is created in the root directory.
- **Final Paper:** Printed to stdout and emitted as an event; not saved to disk.
- **Transcripts:** Logged to `agent.log`; not saved individually.
- **Sandbox Artifacts:** Remain in the Modal sandbox unless printed.

## Proposed Solution

To organize artifacts better and prevent overwriting, we propose creating a structured directory for each run.

### Directory Structure
```
runs/
└── <YYYYMMDD_HHMMSS>_<sanitized_task_name>/
├── agent.log # Full execution log for this run
├── final_paper.md # The generated research paper
├── metadata.json # Run details (task, model, params)
└── agents/
├── agent_1.txt # Transcript for Agent 1
├── agent_2.txt # Transcript for Agent 2
└── ...
```

### Implementation Steps

1. **Initialize Run Directory:**
* In `main.py` (or `orchestrator.py` / `agent.py`), before starting the experiment:
* Generate a timestamp (e.g., `20231027_103000`).
* Sanitize the task name (take first 30 chars, replace non-alphanumeric with `_`).
* Create the directory `runs/<timestamp>_<task_slug>/`.

2. **Update Logging:**
* Modify `logger.py` to accept a `log_file_path` argument in `setup_logging`.
* Initialize the logger to write to the new run directory's `agent.log`.

3. **Save Final Paper:**
* In `orchestrator.py`, after the final paper is generated:
* Write the content to `runs/.../final_paper.md`.

4. **Save Transcripts:**
* In `orchestrator.py`, when a sub-agent completes:
* Write its transcript to `runs/.../agents/agent_<id>.txt`.

5. **Metadata:**
* Save run arguments (task, model, gpu, etc.) to `runs/.../metadata.json` for reproducibility.
130 changes: 129 additions & 1 deletion api_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from typing import Any, Dict, List, Optional, Literal

from dotenv import load_dotenv, set_key
from fastapi import FastAPI, HTTPException
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import StreamingResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, Field
Expand Down Expand Up @@ -783,6 +783,134 @@ def summarize_agent(req: AgentSummaryRequest) -> AgentSummaryResponse:
return AgentSummaryResponse(**result)


@app.get("/api/files/list", summary="List files in a directory")
def list_files(path: str = ".") -> List[Dict[str, Any]]:
"""
List files and directories in the given path relative to the project root.
"""
# Security check: ensure path resolves to inside BASE_DIR
# We strip any leading slashes/dots to prevent traversing up from root initially
clean_path = path.strip("/")
if clean_path == "." or clean_path == "":
target_path = BASE_DIR
else:
target_path = (BASE_DIR / clean_path).resolve()

if not str(target_path).startswith(str(BASE_DIR)):
raise HTTPException(status_code=403, detail="Access denied: Cannot traverse outside project root.")

if not target_path.exists():
raise HTTPException(status_code=404, detail="Path not found.")

if not target_path.is_dir():
raise HTTPException(status_code=400, detail="Path is not a directory.")

results = []
try:
for item in target_path.iterdir():
# Skip hidden files/dirs and .git, and __pycache__
if item.name.startswith(".") or item.name == "__pycache__":
continue

stats = item.stat()
results.append({
"name": item.name,
"path": str(item.relative_to(BASE_DIR)).replace("\\", "/"),
"type": "directory" if item.is_dir() else "file",
"size": stats.st_size,
"modified": datetime.fromtimestamp(stats.st_mtime, timezone.utc).isoformat(),
})
except PermissionError:
raise HTTPException(status_code=403, detail="Permission denied.")

# Sort: directories first, then files
results.sort(key=lambda x: (x["type"] != "directory", x["name"].lower()))
return results


@app.get("/api/files/read", summary="Read a file content")
def read_file(path: str) -> Dict[str, str]:
"""
Read the content of a text file.
"""
clean_path = path.strip("/")
target_path = (BASE_DIR / clean_path).resolve()

if not str(target_path).startswith(str(BASE_DIR)):
raise HTTPException(status_code=403, detail="Access denied.")

if not target_path.exists() or not target_path.is_file():
raise HTTPException(status_code=404, detail="File not found.")

try:
content = target_path.read_text(encoding="utf-8")
return {"content": content}
except UnicodeDecodeError:
raise HTTPException(status_code=400, detail="Binary file not supported for text view.")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@app.get("/api/files/download", summary="Download a file")
def download_file(path: str):
"""
Download a single file.
"""
clean_path = path.strip("/")
target_path = (BASE_DIR / clean_path).resolve()

if not str(target_path).startswith(str(BASE_DIR)):
raise HTTPException(status_code=403, detail="Access denied.")

if not target_path.exists() or not target_path.is_file():
raise HTTPException(status_code=404, detail="File not found.")

return FileResponse(target_path, filename=target_path.name)


@app.get("/api/files/download_zip", summary="Download directory as zip")
def download_zip(path: str = "."):
"""
Download a directory as a zip file.
"""
clean_path = path.strip("/")
if clean_path == "." or clean_path == "":
target_path = BASE_DIR
else:
target_path = (BASE_DIR / clean_path).resolve()

if not str(target_path).startswith(str(BASE_DIR)):
raise HTTPException(status_code=403, detail="Access denied.")

if not target_path.exists() or not target_path.is_dir():
raise HTTPException(status_code=404, detail="Directory not found.")

# Create a zip file in memory
import io
import zipfile

zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
for root, dirs, files in os.walk(target_path):
# exclude hidden folders
dirs[:] = [d for d in dirs if not d.startswith(".") and d != "__pycache__" and d != "node_modules"]

for file in files:
if file.startswith("."):
continue
file_path = Path(root) / file
arcname = file_path.relative_to(target_path)
zip_file.write(file_path, arcname)

zip_buffer.seek(0)
filename = target_path.name if (path != "." and path != "") else "project"
return StreamingResponse(
zip_buffer,
media_type="application/zip",
headers={"Content-Disposition": f"attachment; filename={filename}.zip"}
)


# ---------------------------------------------------------------------------
# Static file serving for production (Railway, etc.)
# ---------------------------------------------------------------------------
Expand Down
18 changes: 17 additions & 1 deletion frontend/package-lock.json

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

Loading