Skip to content

Commit 315503f

Browse files
authored
Merge pull request #35 from BrunoV21/pg-data-layer
Pg data layer
2 parents 61314af + c8c5401 commit 315503f

File tree

4 files changed

+191
-35
lines changed

4 files changed

+191
-35
lines changed

codetide/agents/data_layer.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
from sqlalchemy import String, Text, ForeignKey, Boolean, Integer
44
from sqlalchemy.ext.asyncio import create_async_engine
55
from sqlalchemy.types import TypeDecorator
6+
from sqlalchemy.exc import OperationalError
67
except ImportError as e:
78
raise ImportError(
89
"This module requires 'sqlalchemy' and 'ulid-py'. "
910
"Install them with: pip install codetide[agents-ui]"
10-
) from e
11+
) from e
1112

13+
import asyncio
1214
from datetime import datetime
13-
from sqlalchemy import Select
1415
from ulid import ulid
15-
import asyncio
1616
import json
1717

1818
# SQLite-compatible JSON and UUID types
@@ -169,11 +169,27 @@ class Feedback(Base):
169169
# chats = await db.list_chats()
170170
# for c in chats:
171171
# 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"))
172+
173+
async def init_db(conn_str: str, max_retries: int = 5, retry_delay: int = 2):
174+
"""
175+
Initialize database with retry logic for connection issues.
176+
"""
177+
engine = create_async_engine(conn_str)
178+
179+
for attempt in range(max_retries):
180+
try:
181+
async with engine.begin() as conn:
182+
await conn.run_sync(Base.metadata.create_all)
183+
print("Database initialized successfully!")
184+
return
185+
except OperationalError as e:
186+
if attempt == max_retries - 1:
187+
print(f"Failed to initialize database after {max_retries} attempts: {e}")
188+
raise
189+
else:
190+
print(f"Database connection failed (attempt {attempt + 1}/{max_retries}): {e}")
191+
print(f"Retrying in {retry_delay} seconds...")
192+
await asyncio.sleep(retry_delay)
193+
except Exception as e:
194+
print(f"Unexpected error initializing database: {e}")
195+
raise

codetide/agents/tide/ui/app.py

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
from aicore.config import Config
1010
from aicore.llm import Llm, LlmConfig
1111
from aicore.models import AuthenticationError, ModelError
12-
from aicore.const import STREAM_END_TOKEN, STREAM_START_TOKEN#, REASONING_START_TOKEN, REASONING_STOP_TOKEN
13-
from codetide.agents.tide.ui.utils import process_thread, run_concurrent_tasks, send_reasoning_msg
12+
from aicore.const import STREAM_END_TOKEN, STREAM_START_TOKEN#, REASONING_START_TOKEN, REASONING_STOP_TOKEN
13+
from codetide.agents.tide.ui.utils import process_thread, run_concurrent_tasks, send_reasoning_msg, check_docker, launch_postgres
1414
from codetide.agents.tide.ui.stream_processor import StreamProcessor, MarkerConfig
1515
from codetide.agents.tide.ui.defaults import AGENT_TIDE_PORT, STARTERS
1616
from codetide.agents.tide.ui.agent_tide_ui import AgentTideUi
@@ -24,29 +24,33 @@
2424
except ImportError as e:
2525
raise ImportError(
2626
"The 'codetide.agents' module requires the 'aicore' and 'chainlit' packages. "
27-
"Install it with: pip install codetide[agents-ui]"
27+
"Install it with: pip install codetide[aasygents-ui]"
2828
) from e
2929

3030
from codetide.agents.tide.ui.defaults import AICORE_CONFIG_EXAMPLE, EXCEPTION_MESSAGE, MISSING_CONFIG_MESSAGE
3131
from codetide.agents.tide.defaults import DEFAULT_AGENT_TIDE_LLM_CONFIG_PATH
3232
from codetide.core.defaults import DEFAULT_ENCODING
33+
from dotenv import get_key, load_dotenv, set_key
3334
from codetide.agents.data_layer import init_db
3435
from ulid import ulid
3536
import argparse
3637
import getpass
3738
import asyncio
39+
import secrets
40+
import string
3841
import json
3942
import yaml
4043
import time
4144

42-
@cl.password_auth_callback
43-
def auth():
44-
username = getpass.getuser()
45-
return cl.User(identifier=username, display_name=username)
45+
if check_docker and os.getenv("AGENTTIDE_PG_CONN_STR") is not None:
46+
@cl.password_auth_callback
47+
def auth():
48+
username = getpass.getuser()
49+
return cl.User(identifier=username, display_name=username)
4650

47-
@cl.data_layer
48-
def get_data_layer():
49-
return SQLAlchemyDataLayer(conninfo=f"sqlite+aiosqlite:///{os.environ['CHAINLIT_APP_ROOT']}/database.db")
51+
@cl.data_layer
52+
def get_data_layer():
53+
return SQLAlchemyDataLayer(conninfo=os.getenv("AGENTTIDE_PG_CONN_STR"))
5054

5155
@cl.on_settings_update
5256
async def setup_llm_config(settings):
@@ -442,9 +446,18 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option
442446
chat_history.append({"role": "user", "content": feedback})
443447
await agent_loop(agent_tide_ui=agent_tide_ui)
444448

445-
# def generate_temp_password(length=16):
446-
# characters = string.ascii_letters + string.digits + string.punctuation
447-
# return ''.join(secrets.choice(characters) for _ in range(length))
449+
def generate_password(length: int = 16) -> str:
450+
"""
451+
Generate a secure random password.
452+
Works on Linux, macOS, and Windows.
453+
"""
454+
if password := get_key(Path(os.environ['CHAINLIT_APP_ROOT']) / ".env", "AGENTTDE_PG_PASSWORD"):
455+
return password
456+
457+
safe_chars = string.ascii_letters + string.digits + '-_@#$%^&*+=[]{}|:;<>?'
458+
password = ''.join(secrets.choice(safe_chars) for _ in range(length))
459+
set_key(Path(os.environ['CHAINLIT_APP_ROOT']) / ".env","AGENTTDE_PG_PASSWORD", password)
460+
return password
448461

449462
def serve(
450463
host=None,
@@ -454,14 +467,7 @@ def serve(
454467
ssl_keyfile=None,
455468
ws_per_message_deflate="true",
456469
ws_protocol="auto"
457-
):
458-
username = getpass.getuser()
459-
GREEN = "\033[92m"
460-
RESET = "\033[0m"
461-
462-
print(f"\n{GREEN}Your chainlit username is `{username}`{RESET}\n")
463-
464-
470+
):
465471
# if not os.getenv("_PASSWORD"):
466472
# temp_password = generate_temp_password()
467473
# os.environ["_PASSWORD"] = temp_password
@@ -513,10 +519,33 @@ def main():
513519
parser.add_argument("--config-path", type=str, default=DEFAULT_AGENT_TIDE_LLM_CONFIG_PATH, help="Path to the config file")
514520
args = parser.parse_args()
515521

522+
load_dotenv()
516523
os.environ["AGENT_TIDE_PROJECT_PATH"] = str(Path(args.project_path))
517524
os.environ["AGENT_TIDE_CONFIG_PATH"] = str(Path(args.project_path) / args.config_path)
525+
526+
load_dotenv()
527+
username = getpass.getuser()
528+
GREEN = "\033[92m"
529+
RED = "\033[91m"
530+
RESET = "\033[0m"
531+
532+
print(f"\n{GREEN}Your chainlit username is `{username}`{RESET}\n")
533+
534+
if check_docker():
535+
password = generate_password()
536+
launch_postgres(username, password, f"{os.environ['CHAINLIT_APP_ROOT']}/pgdata")
537+
538+
conn_string = f"postgresql+asyncpg://{username}:{password}@localhost:{os.getenv('AGENTTIDE_PG_PORT', 5437)}/agenttidedb"
539+
os.environ["AGENTTIDE_PG_CONN_STR"] = conn_string
540+
asyncio.run(init_db(os.environ["AGENTTIDE_PG_CONN_STR"]))
518541

519-
asyncio.run(init_db(f"{os.environ['CHAINLIT_APP_ROOT']}/database.db"))
542+
print(f"{GREEN} PostgreSQL launched on port {os.getenv('AGENTTIDE_PG_PORT', 5437)}{RESET}")
543+
print(f"{GREEN} Connection string stored in env var: AGENTTIDE_PG_CONN_STR{RESET}\n")
544+
else:
545+
print(f"{RED} Could not find Docker on this system.{RESET}")
546+
print(" PostgreSQL could not be launched for persistent data storage.")
547+
print(" You won't have access to multiple conversations or history beyond each session.")
548+
print(" Consider installing Docker and ensuring it is running.\n")
520549

521550
serve(
522551
host=args.host,

codetide/agents/tide/ui/utils.py

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22

33
from typing import List, Optional, Tuple
44
from chainlit.types import ThreadDict
5+
from rich.progress import Progress
56
from aicore.logger import _logger
67
from aicore.llm import LlmConfig
78
import chainlit as cl
89
import asyncio
910
import orjson
11+
import docker
1012
import time
13+
import os
14+
1115

1216
def process_thread(thread :ThreadDict)->Tuple[List[dict], Optional[LlmConfig], str]:
1317
### type: tool
@@ -93,4 +97,110 @@ async def send_reasoning_msg(loading_msg :cl.message, context_msg :cl.Message, a
9397
)
9498
)
9599
await context_msg.send()
96-
return True
100+
return True
101+
102+
def check_docker():
103+
try:
104+
client = docker.from_env()
105+
client.ping() # Simple API check
106+
return True
107+
except Exception:
108+
return False
109+
110+
tasks = {}
111+
112+
# Show task progress (red for download, green for extract)
113+
def show_progress(line, progress):
114+
if line['status'] == 'Downloading':
115+
id = f'[red][Download {line["id"]}]'
116+
elif line['status'] == 'Extracting':
117+
id = f'[green][Extract {line["id"]}]'
118+
else:
119+
# skip other statuses
120+
return
121+
122+
if id not in tasks.keys():
123+
tasks[id] = progress.add_task(f"{id}", total=line['progressDetail']['total'])
124+
else:
125+
progress.update(tasks[id], completed=line['progressDetail']['current'])
126+
127+
def image_pull(client :docker.DockerClient, image_name):
128+
print(f'Pulling image: {image_name}')
129+
with Progress() as progress:
130+
resp = client.api.pull(image_name, stream=True, decode=True)
131+
for line in resp:
132+
show_progress(line, progress)
133+
134+
def wait_for_postgres_ready(container, username: str, password: str, max_attempts: int = 30, delay: int = 2) -> bool:
135+
"""
136+
Wait for PostgreSQL to be ready by checking container logs and attempting connections.
137+
"""
138+
print("Waiting for PostgreSQL to be ready...")
139+
140+
for attempt in range(max_attempts):
141+
try:
142+
# First, check if container is still running
143+
container.reload()
144+
if container.status != "running":
145+
print(f"Container stopped unexpectedly. Status: {container.status}")
146+
return False
147+
148+
# Check logs for readiness indicator
149+
logs = container.logs().decode('utf-8')
150+
if "database system is ready to accept connections" in logs:
151+
print("PostgreSQL is ready to accept connections!")
152+
# Give it one more second to be completely ready
153+
time.sleep(5)
154+
return True
155+
156+
print(f"Attempt {attempt + 1}/{max_attempts}: PostgreSQL not ready yet...")
157+
time.sleep(delay)
158+
159+
except Exception as e:
160+
print(f"Error checking PostgreSQL readiness: {e}")
161+
time.sleep(delay)
162+
163+
print("Timeout waiting for PostgreSQL to be ready")
164+
return False
165+
166+
def launch_postgres(POSTGRES_USER: str, POSTGRES_PASSWORD: str, volume_path: str):
167+
client = docker.from_env()
168+
container_name = "agent-tide-postgres"
169+
170+
# Check if the container already exists
171+
try:
172+
container = client.containers.get(container_name)
173+
status = container.status
174+
print(f"Container '{container_name}' status: {status}")
175+
if status == "running":
176+
print("Container is already running. No need to relaunch.")
177+
return
178+
else:
179+
print("Container exists but is not running. Starting container...")
180+
container.start()
181+
return
182+
except docker.errors.NotFound:
183+
# Container does not exist, we need to create it
184+
print("Container does not exist. Launching a new one...")
185+
186+
187+
image_pull(client, "postgres:alpine")
188+
print("Image pulled successfully")
189+
# Launch a new container
190+
container = client.containers.run(
191+
"postgres:alpine",
192+
name=container_name,
193+
environment={
194+
"POSTGRES_USER": POSTGRES_USER,
195+
"POSTGRES_PASSWORD": POSTGRES_PASSWORD,
196+
"POSTGRES_DB": "agenttidedb"
197+
},
198+
ports={"5432/tcp": os.getenv('AGENTTIDE_PG_PORT', 5437)},
199+
volumes={volume_path: {"bind": "/var/lib/postgresql/data", "mode": "rw"}},
200+
detach=True,
201+
restart_policy={"Name": "always"}
202+
)
203+
204+
print(f"Container '{container_name}' launched successfully with status: {container.status}")
205+
# Wait for PostgreSQL to be ready
206+
return wait_for_postgres_ready(container, POSTGRES_USER, POSTGRES_PASSWORD)

requirements-agents-ui.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
chainlit==2.6.3
22
SQLAlchemy==2.0.36
3-
aiosqlite==0.21.0
3+
asyncpg==0.30.0
4+
docker==7.1.0

0 commit comments

Comments
 (0)