Skip to content

Commit fdcdd30

Browse files
authored
Merge pull request #6 from BrunoV21/diff-match-path
Diff match path
2 parents 09a2f95 + d472531 commit fdcdd30

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+9492
-594
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Sync Agent Tide Demo to Hugging Face Space
2+
on:
3+
push:
4+
branches: [main]
5+
tags: [ "*" ]
6+
release:
7+
types: [published, created, edited]
8+
9+
workflow_dispatch:
10+
11+
jobs:
12+
sync-to-hub:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout repo
16+
uses: actions/checkout@v3
17+
with:
18+
fetch-depth: 0
19+
lfs: true
20+
21+
- name: Deploy examples/hf_demo_space to HF Space
22+
env:
23+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
24+
run: |
25+
cd examples/hf_demo_space
26+
git init --initial-branch=main
27+
git config user.name "github-actions[bot]"
28+
git config user.email "github-actions[bot]@users.noreply.github.com"
29+
git remote add origin https://McLoviniTtt:$HF_TOKEN@huggingface.co/spaces/McLoviniTtt/AgentTideDemo
30+
git add .
31+
git commit -m "Deploy Agent Tide Demo to HF Space"
32+
git push --force origin main

.github/workflows/python_package.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ jobs:
4444
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
4545
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
4646
- name: Test with pytest
47-
run: pytest tests --ignore=tests/agents
47+
run: pytest tests --ignore=tests/agents --ignore=tests/mcp/tools
4848
- name: Install package with agents requirements
4949
run: |
50-
python -m pip install .[agents]
50+
python -m pip install -r requirements-agents.txt
5151
- name: Test agents with pytest
52-
run: pytest tests/agents
52+
run: pytest tests/agents tests/mcp/tools
5353
security:
5454
name: Security Check
5555
runs-on: ubuntu-latest

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,7 @@ storage/
177177
logs/
178178
observability_data/
179179
config/
180+
*.db
181+
.files/
180182

183+
codetide/agents/tide/ui/assets/

MANIFEST.in

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
include requirements.txt
2+
include requirements-agents.txt
3+
include docs/assets/codetide-logo.png
4+
include requirements-visualization.txt
5+
include README.md
6+
7+
# Include all files in codetide/agents/tide/ui/.chainlit and public directories (and subdirectories)
8+
recursive-include codetide/agents/tide/ui/.chainlit *
9+
recursive-include codetide/agents/tide/ui/public *
10+
recursive-include codetide/agents/tide/ui *

MANIFIEST.in

Lines changed: 0 additions & 5 deletions
This file was deleted.

README.md

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1+
<div align="center">
2+
13
<!-- [![Docs](https://img.shields.io/badge/docs-CodeTide.github.io-red)](https://brunov21.github.io/CodeTide/) -->
2-
# <img src="./docs/assets/codetide-logo.png" alt="code-tide-logo" width="35" height="auto"/> CodeTide
4+
<img src="./codetide/agents/tide/ui/public/codetide-banner.png" alt="code-tide-logo" width="900" height="auto"/>
5+
6+
7+
38
[![GitHub Stars](https://img.shields.io/github/stars/BrunoV21/CodeTide?style=social)](https://github.com/BrunoV21/CodeTide/stargazers)
49
[![PyPI Downloads](https://static.pepy.tech/badge/CodeTide)](https://pepy.tech/projects/CodeTide)
510
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/CodeTide?style=flat)](https://pypi.org/pypi/codetide/)
611
[![PyPI - Version](https://img.shields.io/pypi/v/CodeTide?style=flat)](https://pypi.org/pypi/codetide/)
712
[![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json)](https://pydantic.dev)
8-
---
913

10-
**CodeTide** is a fully local, privacy-preserving tool for parsing and understanding Python codebases using symbolic, structural analysis. No internet, no LLMs, no embeddings - just fast, explainable, and deterministic code intelligence.
14+
</div>
1115

1216
---
1317

18+
**CodeTide** is a fully local, privacy-preserving tool for parsing and understanding Python codebases using symbolic, structural analysis. No internet, no LLMs, no embeddings - just fast, explainable, and deterministic code intelligence.
19+
20+
1421
## ✅ Key Features
1522

1623
- ✅ 100% **local & private** - all parsing and querying happens on your machine.
@@ -20,6 +27,64 @@
2027
- ⚡ Fast, cacheable parsing with smart update detection.
2128
- 🔁 Designed to work alongside tools like Copilot, GPT, and Claude - on your terms.
2229

30+
31+
---
32+
# Entrypoints & Usage
33+
34+
CodeTide provides several entrypoints for interacting with the system via command-line and web UI. These entrypoints are exposed through the `uvx` launcher and require the appropriate extras to be installed.
35+
36+
## CodeTide CLI
37+
38+
To use the main CodeTide CLI:
39+
40+
```sh
41+
uvx --from codetide codetide-cli --help
42+
```
43+
## AgentTide
44+
45+
AgentTide consists of a demo, showing how CodeTide can integrate with LLMs and augment code generation and condebase related workflows. If you ask Tide to describe himself, he will say something like this: I'm the next-generation, precision-driven software engineering agent built on top of CodeTide. You can use it via the command-line interface (CLI) or a beautiful interactive UI.
46+
47+
---
48+
49+
<div align="center">
50+
<!-- [![Docs](https://img.shields.io/badge/docs-CodeTide.github.io-red)](https://brunov21.github.io/CodeTide/) -->
51+
<img src="./codetide/agents/tide/ui/public/agent-tide-demo.gif" alt="agent-tide-demo" width="100%" height="auto"/>
52+
</div>
53+
54+
---
55+
56+
**AgentTide CLI**
57+
58+
To use the AgentTide conversational CLI, you must install the `[agents]` extra and launch via:
59+
60+
```sh
61+
uvx --from codetide[agents] agent-tide
62+
```
63+
64+
This will start an interactive terminal session with AgentTide.
65+
66+
**AgentTide UI**
67+
68+
To use the AgentTide web UI, you must install the `[agents-ui]` extra and launch via:
69+
70+
```sh
71+
uvx --from codetide[agents-ui] agent-tide-ui
72+
```
73+
74+
This will start a web server for the AgentTide UI. Follow the on-screen instructions to interact with the agent in your browser at [http://localhost:9753](http://localhost:9753) (or the port you specified)
75+
76+
### Why Try AgentTide? ([Full Guide & Tips Here](codetide/agents/tide/ui/chainlit.md))
77+
78+
**Local-First & Private:** All code analysis and patching is performed locally. Your code never leaves your machine.
79+
- **Transparent & Stepwise:** See every plan and patch before it's applied. Edit, reorder, or approve steps—you're always in control.
80+
- **Context-Aware:** AgentTide loads only the relevant code identifiers and dependencies for your request, making it fast and precise.
81+
- **Human-in-the-Loop:** After each step, review the patch, provide feedback, or continue—no black-box agent behavior.
82+
- **Patch-Based Editing:** All changes are atomic diffs, not full file rewrites, for maximum clarity and efficiency.
83+
84+
**Usage Tips:**
85+
If you know the exact code context, specify identifiers directly in your request (e.g., `module.submodule.file_withoutextension.object`).
86+
You can request a plan, edit steps, and proceed step-by-step—see the [chainlit.md](codetide/agents/tide/ui/chainlit.md) for full details and advanced workflows!
87+
2388
---
2489

2590
## 🔌 VSCode Extension
@@ -461,6 +526,9 @@ Here’s what’s next for CodeTide:
461526
~~- 🧭 **Handle relative imports** in Python projects
462527
→ Improve resolution for intra-package navigation.~~
463528
529+
- 🚀 **AgentTideUi Hugging Face Space**
530+
→ We are planning to make AgentTideUi available as a Hugging Face Space, supporting GitHub OAuth for user session and allowing users to provide a repo URL for one-time conversations.
531+
464532
---
465533
466534
## 🤖 Agents Module: AgentTide

codetide/__init__.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from codetide.core.defaults import (
22
CODETIDE_ASCII_ART, DEFAULT_SERIALIZATION_PATH, DEFAULT_MAX_CONCURRENT_TASKS,
33
DEFAULT_BATCH_SIZE, DEFAULT_CACHED_ELEMENTS_FILE, DEFAULT_CACHED_IDS_FILE,
4-
LANGUAGE_EXTENSIONS
4+
LANGUAGE_EXTENSIONS, SKIP_EXTENSIONS
55
)
66
from codetide.core.models import CodeFileModel, CodeBase, CodeContextStructure
77
from codetide.core.common import readFile, writeFile
@@ -474,6 +474,18 @@ async def check_for_updates(self,
474474
include_cached_ids=kwargs.get("include_cached_ids", False)
475475
)
476476

477+
@staticmethod
478+
def _is_file_content_valid(filepath :Path)->bool:
479+
# Lowercase name for case-insensitive matching
480+
name_lower = filepath.name.lower()
481+
482+
# Skip if extension or full filename is in SKIP_EXTENSIONS
483+
for ext in SKIP_EXTENSIONS:
484+
if name_lower.endswith(ext.lower()):
485+
return False
486+
487+
return True
488+
477489
def _precheck_id_is_file(self, unique_ids : List[str])->Dict[Path, str]:
478490
"""
479491
Preload file contents for the given IDs if they correspond to known files.
@@ -486,7 +498,7 @@ def _precheck_id_is_file(self, unique_ids : List[str])->Dict[Path, str]:
486498
"""
487499
return {
488500
unique_id: readFile(self.rootpath / unique_id) for unique_id in unique_ids
489-
if self.rootpath / unique_id in self.files
501+
if self.rootpath / unique_id in self.files and self._is_file_content_valid(self.rootpath / unique_id)
490502
}
491503

492504
def get(

codetide/agents/data_layer.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
try:
2+
from sqlalchemy.orm import declarative_base, relationship, mapped_column
3+
from sqlalchemy import String, Text, ForeignKey, Boolean, Integer
4+
from sqlalchemy.ext.asyncio import create_async_engine
5+
from sqlalchemy.types import TypeDecorator
6+
except ImportError as e:
7+
raise ImportError(
8+
"This module requires 'sqlalchemy' and 'ulid-py'. "
9+
"Install them with: pip install codetide[agents-ui]"
10+
) from e
11+
12+
from datetime import datetime
13+
from sqlalchemy import Select
14+
from ulid import ulid
15+
import asyncio
16+
import json
17+
18+
# SQLite-compatible JSON and UUID types
19+
class GUID(TypeDecorator):
20+
impl = String
21+
22+
def process_bind_param(self, value, dialect):
23+
if value is None:
24+
return None
25+
return str(value)
26+
27+
def process_result_value(self, value, dialect):
28+
return value
29+
class JSONEncodedDict(TypeDecorator):
30+
impl = Text
31+
32+
def process_bind_param(self, value, dialect):
33+
return json.dumps(value) if value is not None else None
34+
35+
def process_result_value(self, value, dialect):
36+
return json.loads(value) if value is not None else None
37+
38+
class JSONEncodedList(TypeDecorator):
39+
impl = Text
40+
41+
def process_bind_param(self, value, dialect):
42+
return json.dumps(value) if value is not None else None
43+
44+
def process_result_value(self, value, dialect):
45+
return json.loads(value) if value is not None else None
46+
47+
Base = declarative_base()
48+
49+
class User(Base):
50+
__tablename__ = "users"
51+
id = mapped_column(GUID, primary_key=True, default=ulid)
52+
identifier = mapped_column(Text, unique=True, nullable=False)
53+
user_metadata = mapped_column("metadata", JSONEncodedDict, nullable=False)
54+
createdAt = mapped_column(Text, default=lambda: datetime.utcnow().isoformat())
55+
56+
class Thread(Base):
57+
__tablename__ = "threads"
58+
id = mapped_column(GUID, primary_key=True, default=ulid)
59+
createdAt = mapped_column(Text, default=lambda: datetime.utcnow().isoformat())
60+
name = mapped_column(Text)
61+
userId = mapped_column(GUID, ForeignKey("users.id", ondelete="CASCADE"))
62+
userIdentifier = mapped_column(Text)
63+
tags = mapped_column(JSONEncodedList)
64+
user_metadata = mapped_column("metadata", JSONEncodedDict)
65+
66+
user = relationship("User", backref="threads")
67+
68+
class Step(Base):
69+
__tablename__ = "steps"
70+
id = mapped_column(GUID, primary_key=True, default=ulid)
71+
name = mapped_column(Text, nullable=False)
72+
type = mapped_column(Text, nullable=False)
73+
threadId = mapped_column(GUID, ForeignKey("threads.id", ondelete="CASCADE"), nullable=False)
74+
parentId = mapped_column(GUID)
75+
streaming = mapped_column(Boolean, nullable=False)
76+
waitForAnswer = mapped_column(Boolean)
77+
isError = mapped_column(Boolean)
78+
user_metadata = mapped_column("metadata", JSONEncodedDict)
79+
tags = mapped_column(JSONEncodedList)
80+
input = mapped_column(Text)
81+
output = mapped_column(Text)
82+
createdAt = mapped_column(Text, default=lambda: datetime.utcnow().isoformat())
83+
command = mapped_column(Text)
84+
start = mapped_column(Text)
85+
end = mapped_column(Text)
86+
generation = mapped_column(JSONEncodedDict)
87+
showInput = mapped_column(Text)
88+
language = mapped_column(Text)
89+
indent = mapped_column(Integer)
90+
defaultOpen = mapped_column(Boolean, default=False)
91+
92+
class Element(Base):
93+
__tablename__ = "elements"
94+
id = mapped_column(GUID, primary_key=True, default=ulid)
95+
threadId = mapped_column(GUID, ForeignKey("threads.id", ondelete="CASCADE"))
96+
type = mapped_column(Text)
97+
url = mapped_column(Text)
98+
chainlitKey = mapped_column(Text)
99+
name = mapped_column(Text, nullable=False)
100+
display = mapped_column(Text)
101+
objectKey = mapped_column(Text)
102+
size = mapped_column(Text)
103+
page = mapped_column(Integer)
104+
language = mapped_column(Text)
105+
forId = mapped_column(GUID)
106+
mime = mapped_column(Text)
107+
props = mapped_column(JSONEncodedDict)
108+
109+
class Feedback(Base):
110+
__tablename__ = "feedbacks"
111+
id = mapped_column(GUID, primary_key=True, default=ulid)
112+
forId = mapped_column(GUID, nullable=False)
113+
threadId = mapped_column(GUID, ForeignKey("threads.id", ondelete="CASCADE"), nullable=False)
114+
value = mapped_column(Integer, nullable=False)
115+
comment = mapped_column(Text)
116+
117+
# class AsyncMessageDB:
118+
# def __init__(self, db_path: str):
119+
# self.db_url = f"sqlite+aiosqlite:///{db_path}"
120+
# self.engine = create_async_engine(self.db_url, echo=False)
121+
# self.async_session = async_sessionmaker(bind=self.engine, class_=AsyncSession, expire_on_commit=False)
122+
123+
# async def init_db(self):
124+
# async with self.engine.begin() as conn:
125+
# await conn.run_sync(Base.user_metadata.create_all)
126+
127+
# async def create_chat(self, name: str) -> Chat:
128+
# async with self.async_session() as session:
129+
# chat = Chat(name=name)
130+
# session.add(chat)
131+
# await session.commit()
132+
# await session.refresh(chat)
133+
# return chat
134+
135+
# async def add_message(self, chat_id: str, role: str, content: str) -> Message:
136+
# async with self.async_session() as session:
137+
# message = Message(chat_id=chat_id, role=role, content=content)
138+
# session.add(message)
139+
# await session.commit()
140+
# await session.refresh(message)
141+
# return message
142+
143+
# async def get_messages_for_chat(self, chat_id: str) -> List[Message]:
144+
# async with self.async_session() as session:
145+
# result = await session.execute(
146+
# select(Message).where(Message.chat_id == chat_id).order_by(Message.timestamp)
147+
# )
148+
# return result.scalars().all()
149+
150+
# async def list_chats(self) -> List[Chat]:
151+
# async with self.async_session() as session:
152+
# result = await session.execute(select(Chat).order_by(Chat.name))
153+
# return result.scalars().all()
154+
155+
# async def main():
156+
# db = AsyncMessageDB(str(Path(os.path.abspath(__file__)).parent / "my_messages.db"))
157+
# await db.init_db()
158+
159+
# chat = await db.create_chat("My First Chat")
160+
# await db.add_message(chat.id, "user", "Hello Assistant!")
161+
# await db.add_message(chat.id, "assistant", "Hello, how can I help you?")
162+
163+
# print(f"Messages for chat '{chat.name}':")
164+
# messages = await db.get_messages_for_chat(chat.id)
165+
# for msg in messages:
166+
# print(f"[{msg.timestamp}] {msg.role.upper()}: {msg.content}")
167+
168+
# print("\nAll chats:")
169+
# chats = await db.list_chats()
170+
# for c in chats:
171+
# print(f"{c.id} — {c.name}")
172+
async def init_db(path: str):
173+
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
174+
engine = create_async_engine(f"sqlite+aiosqlite:///{path}")
175+
async with engine.begin() as conn:
176+
await conn.run_sync(Base.metadata.create_all)
177+
178+
if __name__ == "__main__":
179+
asyncio.run(init_db("database.db"))

0 commit comments

Comments
 (0)