Skip to content

Commit 5861a51

Browse files
authored
Merge pull request #10 from BrunoV21/hf-space-demo
Hf space demo
2 parents 7403747 + 5d4ead7 commit 5861a51

File tree

7 files changed

+176
-44
lines changed

7 files changed

+176
-44
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,5 @@ config/
181181
.files/
182182

183183
codetide/agents/tide/ui/assets/
184+
examples/hf_demo_space/.chainlit/*
185+
examples/hf_demo_space/chainlit.md

codetide/agents/tide/agent.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,9 @@ def _(event):
169169
# This can happen if the event loop is shut down
170170
pass
171171
finally:
172-
_logger.logger.info("Exited by user. Goodbye!")
172+
_logger.logger.info("Exited by user. Goodbye!")
173+
174+
async def _handle_commands(self, command :str):
175+
# TODO add logic here to handlle git command, i.e stage files, write commit messages and checkout
176+
# expand to support new branches
177+
pass

codetide/agents/tide/ui/agent_tide_ui.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,5 +126,7 @@ def settings(self):
126126
)
127127
]
128128

129-
def get_command_prompt(self, command :str)->Optional[str]:
130-
return self.commands_prompts.get(command)
129+
async def get_command_prompt(self, command :str)->Optional[str]:
130+
await self.agent_tide._handle_commands(command)
131+
132+
return self.commands_prompts.get(command)

codetide/agents/tide/ui/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ async def agent_loop(message: cl.Message, codeIdentifiers: Optional[list] = None
240240
chat_history = cl.user_session.get("chat_history")
241241

242242
if message.command:
243-
command_prompt = agent_tide_ui.get_command_prompt(message.command)
243+
command_prompt = await agent_tide_ui.get_command_prompt(message.command)
244244
if command_prompt:
245245
message.content = "\n\n---\n\n".join([command_prompt, message.content])
246246

examples/hf_demo_space/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ RUN mkdir -p /app/.files && chmod 777 /app/.files
1717
RUN mkdir -p /app/logs && chmod 777 /app/logs
1818
RUN mkdir -p /app/observability_data && chmod 777 /app/observability_data
1919
RUN mkdir -p /app/storage && chmod 777 /app/storage
20+
RUN mkdir -p /app/sessions && chmod 777 /app/sessions
2021

2122
# Copy the current repository into the container
2223
COPY . /app

examples/hf_demo_space/app.py

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,54 +10,30 @@
1010
from codetide.agents.tide.ui.utils import run_concurrent_tasks
1111
from codetide.agents.tide.ui.agent_tide_ui import AgentTideUi
1212
from codetide.core.defaults import DEFAULT_ENCODING
13+
from codetide.core.logs import logger
1314
from codetide.agents.tide.models import Step
1415

1516
from aicore.const import STREAM_END_TOKEN, STREAM_START_TOKEN
1617
from aicore.models import AuthenticationError, ModelError
1718
from aicore.config import Config
1819
from aicore.llm import Llm
1920

20-
from typing import Optional
21+
from git_utils import commit_and_push_changes, validate_git_url
2122
from chainlit.cli import run_chainlit
23+
from typing import Optional
2224
from pathlib import Path
2325
from ulid import ulid
2426
import chainlit as cl
2527
import subprocess
28+
import asyncio
2629
import shutil
27-
import yaml
2830
import json
31+
import stat
32+
import yaml
2933
import os
30-
import re
3134

3235
DEFAULT_SESSIONS_WORKSPACE = Path(os.getcwd()) / "sessions"
3336

34-
GIT_URL_PATTERN = re.compile(
35-
r'^(?:http|https|git|ssh)://' # Protocol
36-
r'(?:\S+@)?' # Optional username
37-
r'([^/]+)' # Domain
38-
r'(?:[:/])([^/]+/[^/]+?)(?:\.git)?$' # Repo path
39-
)
40-
41-
def validate_git_url(url) -> None:
42-
"""Validate the Git repository URL using git ls-remote."""
43-
if not GIT_URL_PATTERN.match(url):
44-
raise ValueError(f"Invalid Git repository URL format: {url}")
45-
46-
try:
47-
result = subprocess.run(
48-
["git", "ls-remote", url],
49-
capture_output=True,
50-
text=True,
51-
check=True,
52-
timeout=10 # Add timeout to prevent hanging
53-
)
54-
if not result.stdout.strip():
55-
raise ValueError(f"URL {url} points to an empty repository")
56-
except subprocess.TimeoutExpired:
57-
raise ValueError(f"Timeout while validating URL {url}")
58-
except subprocess.CalledProcessError as e:
59-
raise ValueError(f"Invalid Git repository URL: {url}. Error: {e.stderr}") from e
60-
6137
async def validate_llm_config_hf(agent_tide_ui: AgentTideUi):
6238
exception = True
6339
session_id = cl.user_session.get("session_id")
@@ -141,22 +117,35 @@ async def clone_repo(session_id):
141117

142118
while exception:
143119
try:
144-
url = await cl.AskUserMessage(
120+
user_message = await cl.AskUserMessage(
145121
content="Provide a valid github url to give AgentTide some context!"
146122
).send()
147-
validate_git_url(url)
123+
url = user_message.get("output")
124+
await validate_git_url(url)
148125
exception = None
149126
except Exception as e:
150127
await cl.Message(f"Invalid url found, please provide only the url, if it is a private repo you can inlucde a PAT in the url: {e}").send()
151128
exception = e
152129

153-
subprocess.run(
154-
["git", "clone", "--no-checkout", url, DEFAULT_SESSIONS_WORKSPACE / session_id],
155-
check=True,
156-
capture_output=True,
157-
text=True,
158-
timeout=300
130+
logger.info(f"executing cmd git clone --no-checkout {url} {DEFAULT_SESSIONS_WORKSPACE / session_id}")
131+
132+
process = await asyncio.create_subprocess_exec(
133+
"git", "clone", url, str(DEFAULT_SESSIONS_WORKSPACE / session_id),
134+
stdout=asyncio.subprocess.PIPE,
135+
stderr=asyncio.subprocess.PIPE
159136
)
137+
138+
try:
139+
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=300)
140+
except asyncio.TimeoutError:
141+
process.kill()
142+
await process.wait()
143+
raise
144+
145+
if process.returncode != 0:
146+
raise subprocess.CalledProcessError(process.returncode, ["git", "clone", url], stdout, stderr)
147+
148+
logger.info(f"finished cloning to {DEFAULT_SESSIONS_WORKSPACE / session_id}")
160149

161150

162151
@cl.on_chat_start
@@ -178,10 +167,15 @@ async def empty_current_session():
178167
if os.path.exists(session_path):
179168
shutil.rmtree(session_path)
180169

170+
def remove_readonly(func, path, _):
171+
"""Clear the readonly bit and reattempt the removal"""
172+
os.chmod(path, stat.S_IWRITE)
173+
func(path)
174+
181175
@cl.on_app_shutdown
182176
async def empty_all_sessions():
183177
if os.path.exists(DEFAULT_SESSIONS_WORKSPACE):
184-
shutil.rmtree(DEFAULT_SESSIONS_WORKSPACE)
178+
shutil.rmtree(DEFAULT_SESSIONS_WORKSPACE, onexc=remove_readonly)
185179

186180
@cl.action_callback("execute_steps")
187181
async def on_execute_steps(action :cl.Action):
@@ -249,14 +243,19 @@ async def on_stop_steps(action :cl.Action):
249243
agent_tide_ui.current_step = None
250244
await task_list.remove()
251245

246+
@cl.action_callback("checkout_commit_push")
247+
async def on_checkout_commit_push(action :cl.Action):
248+
session_id = cl.user_session.get("session_id")
249+
await commit_and_push_changes(DEFAULT_SESSIONS_WORKSPACE / session_id)
250+
252251
@cl.on_message
253252
async def agent_loop(message: cl.Message, codeIdentifiers: Optional[list] = None):
254253
agent_tide_ui = await loadAgentTideUi()
255254

256255
chat_history = cl.user_session.get("chat_history")
257256

258257
if message.command:
259-
command_prompt = agent_tide_ui.get_command_prompt(message.command)
258+
command_prompt = await agent_tide_ui.get_command_prompt(message.command)
260259
if command_prompt:
261260
message.content = "\n\n---\n\n".join([command_prompt, message.content])
262261

@@ -314,6 +313,12 @@ async def agent_loop(message: cl.Message, codeIdentifiers: Optional[list] = None
314313
tooltip="Next step",
315314
icon="fast-forward",
316315
payload={"msg_id": msg.id}
316+
),
317+
cl.Action(
318+
name="checkout_commit_push",
319+
tooltip="A new branch will be created and the changes made so far will be commited and pushed to the upstream repository",
320+
icon="circle-fading-arrow-up",
321+
payload={"msg_id": msg.id}
317322
)
318323
]
319324

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import asyncio
2+
from pathlib import Path
3+
import re
4+
import subprocess
5+
6+
7+
GIT_URL_PATTERN = re.compile(
8+
r'^(?:http|https|git|ssh)://' # Protocol
9+
r'(?:\S+@)?' # Optional username
10+
r'([^/]+)' # Domain
11+
r'(?:[:/])([^/]+/[^/]+?)(?:\.git)?$' # Repo path
12+
)
13+
14+
async def validate_git_url(url) -> None:
15+
"""Validate the Git repository URL using git ls-remote."""
16+
17+
if not GIT_URL_PATTERN.match(url):
18+
raise ValueError(f"Invalid Git repository URL format: {url}")
19+
20+
try:
21+
process = await asyncio.create_subprocess_exec(
22+
"git", "ls-remote", url,
23+
stdout=asyncio.subprocess.PIPE,
24+
stderr=asyncio.subprocess.PIPE
25+
)
26+
27+
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=10)
28+
29+
if process.returncode != 0:
30+
raise subprocess.CalledProcessError(process.returncode, ["git", "ls-remote", url], stdout, stderr)
31+
32+
if not stdout.strip():
33+
raise ValueError(f"URL {url} points to an empty repository")
34+
35+
except asyncio.TimeoutError:
36+
process.kill()
37+
await process.wait()
38+
raise ValueError(f"Timeout while validating URL {url}")
39+
except subprocess.CalledProcessError as e:
40+
raise ValueError(f"Invalid Git repository URL: {url}. Error: {e.stderr}") from e
41+
42+
async def commit_and_push_changes(repo_path: Path, branch_name: str = None, commit_message: str = "Auto-commit: Save changes") -> None:
43+
"""Add all changes, commit with default message, and push to remote."""
44+
45+
repo_path_str = str(repo_path)
46+
47+
try:
48+
# Create new branch with Agent Tide + ULID name if not provided
49+
if not branch_name:
50+
import ulid
51+
branch_name = f"agent-tide-{ulid.new()}"
52+
53+
# Create and checkout new branch
54+
process = await asyncio.create_subprocess_exec(
55+
"git", "checkout", "-b", branch_name,
56+
cwd=repo_path_str,
57+
stdout=asyncio.subprocess.PIPE,
58+
stderr=asyncio.subprocess.PIPE,
59+
text=True
60+
)
61+
62+
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=10)
63+
64+
if process.returncode != 0:
65+
raise subprocess.CalledProcessError(process.returncode, ["git", "checkout", "-b", branch_name], stdout, stderr)
66+
67+
# Add all changes
68+
process = await asyncio.create_subprocess_exec(
69+
"git", "add", ".",
70+
cwd=repo_path_str,
71+
stdout=asyncio.subprocess.PIPE,
72+
stderr=asyncio.subprocess.PIPE,
73+
text=True
74+
)
75+
76+
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=30)
77+
78+
if process.returncode != 0:
79+
raise subprocess.CalledProcessError(process.returncode, ["git", "add", "."], stdout, stderr)
80+
81+
# Commit changes
82+
process = await asyncio.create_subprocess_exec(
83+
"git", "commit", "-m", commit_message,
84+
cwd=repo_path_str,
85+
stdout=asyncio.subprocess.PIPE,
86+
stderr=asyncio.subprocess.PIPE,
87+
text=True
88+
)
89+
90+
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=30)
91+
92+
if process.returncode != 0:
93+
# Check if it's because there are no changes to commit
94+
if "nothing to commit" in stderr or "nothing to commit" in stdout:
95+
return # No changes to commit, exit gracefully
96+
raise subprocess.CalledProcessError(process.returncode, ["git", "commit", "-m", commit_message], stdout, stderr)
97+
98+
# Push to remote
99+
process = await asyncio.create_subprocess_exec(
100+
"git", "push", "origin", branch_name,
101+
cwd=repo_path_str,
102+
stdout=asyncio.subprocess.PIPE,
103+
stderr=asyncio.subprocess.PIPE,
104+
text=True
105+
)
106+
107+
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=60)
108+
109+
if process.returncode != 0:
110+
raise subprocess.CalledProcessError(process.returncode, ["git", "push", "origin", branch_name], stdout, stderr)
111+
112+
except asyncio.TimeoutError:
113+
process.kill()
114+
await process.wait()
115+
raise ValueError(f"Timeout during git operation in {repo_path}")
116+
except subprocess.CalledProcessError as e:
117+
raise ValueError(f"Git operation failed in {repo_path}. Error: {e.stderr}") from e

0 commit comments

Comments
 (0)