slide-narrator 5.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
narrator/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """
2
+ The Narrator - Thread and file storage components for conversational AI
3
+ """
4
+
5
+ from .database.thread_store import ThreadStore
6
+ from .storage.file_store import FileStore
7
+ from .models.thread import Thread
8
+ from .models.message import Message
9
+ from .models.attachment import Attachment
10
+
11
+ __version__ = "5.5.0"
12
+ __all__ = [
13
+ "ThreadStore",
14
+ "FileStore",
15
+ "Thread",
16
+ "Message",
17
+ "Attachment",
18
+ ]
@@ -0,0 +1,8 @@
1
+ """
2
+ Database package for Tyler Stores
3
+ """
4
+
5
+ from .thread_store import ThreadStore
6
+ from .models import ThreadRecord, MessageRecord
7
+
8
+ __all__ = ["ThreadStore", "ThreadRecord", "MessageRecord"]
@@ -0,0 +1,222 @@
1
+ """Database CLI for Tyler Stores"""
2
+ import asyncio
3
+ import os
4
+ import click
5
+ import functools
6
+ import subprocess
7
+ import tempfile
8
+ import time
9
+ from pathlib import Path
10
+ from .thread_store import ThreadStore
11
+ from ..utils.logging import get_logger
12
+
13
+ logger = get_logger(__name__)
14
+
15
+ @click.group()
16
+ def main():
17
+ """Narrator CLI - Database management commands"""
18
+ pass
19
+
20
+ @main.command()
21
+ @click.option('--database-url', help='Database URL for initialization')
22
+ def init(database_url):
23
+ """Initialize database tables"""
24
+ async def _init():
25
+ try:
26
+ # Use provided URL or check environment variable
27
+ url = database_url or os.environ.get('NARRATOR_DATABASE_URL')
28
+
29
+ if url:
30
+ store = await ThreadStore.create(url)
31
+ else:
32
+ # Use in-memory storage
33
+ store = await ThreadStore.create()
34
+
35
+ logger.info("Database initialized successfully")
36
+ click.echo("Database initialized successfully")
37
+ except Exception as e:
38
+ logger.error(f"Failed to initialize database: {e}")
39
+ click.echo(f"Error: Failed to initialize database: {e}")
40
+ raise click.Abort()
41
+
42
+ asyncio.run(_init())
43
+
44
+ @main.command()
45
+ @click.option('--database-url', help='Database URL')
46
+ def status(database_url):
47
+ """Check database status"""
48
+ async def _status():
49
+ try:
50
+ # Use provided URL or check environment variable
51
+ url = database_url or os.environ.get('NARRATOR_DATABASE_URL')
52
+
53
+ if url:
54
+ store = await ThreadStore.create(url)
55
+ else:
56
+ store = await ThreadStore.create()
57
+
58
+ # Get some basic stats
59
+ threads = await store.list_recent(limit=5)
60
+ click.echo(f"Database connection: OK")
61
+ click.echo(f"Recent threads count: {len(threads)}")
62
+
63
+ except Exception as e:
64
+ logger.error(f"Database status check failed: {e}")
65
+ click.echo(f"Error: Database status check failed: {e}")
66
+ raise click.Abort()
67
+
68
+ asyncio.run(_status())
69
+
70
+ @main.command()
71
+ @click.option('--port', help='Port to expose PostgreSQL on (default: 5432 or NARRATOR_DB_PORT)')
72
+ @click.option('--detach/--no-detach', default=True, help='Run container in background (default: True)')
73
+ def docker_start(port, detach):
74
+ """Start a PostgreSQL container for Narrator"""
75
+ # Use environment variables with defaults matching docker-compose.yml
76
+ db_name = os.environ.get('NARRATOR_DB_NAME', 'narrator')
77
+ db_user = os.environ.get('NARRATOR_DB_USER', 'narrator')
78
+ db_password = os.environ.get('NARRATOR_DB_PASSWORD', 'narrator_dev')
79
+ db_port = port or os.environ.get('NARRATOR_DB_PORT', '5432')
80
+
81
+ docker_compose_content = f"""services:
82
+ postgres:
83
+ image: postgres:16
84
+ container_name: narrator-postgres
85
+ environment:
86
+ POSTGRES_DB: {db_name}
87
+ POSTGRES_USER: {db_user}
88
+ POSTGRES_PASSWORD: {db_password}
89
+ ports:
90
+ - "{db_port}:5432"
91
+ volumes:
92
+ - narrator_postgres_data:/var/lib/postgresql/data
93
+ healthcheck:
94
+ test: ["CMD-SHELL", "pg_isready -U {db_user}"]
95
+ interval: 5s
96
+ timeout: 5s
97
+ retries: 5
98
+
99
+ volumes:
100
+ narrator_postgres_data:
101
+ """
102
+
103
+ # Create a temporary directory for docker-compose.yml
104
+ with tempfile.TemporaryDirectory() as tmpdir:
105
+ compose_file = Path(tmpdir) / "docker-compose.yml"
106
+ compose_file.write_text(docker_compose_content)
107
+
108
+ # Check if docker is available
109
+ try:
110
+ subprocess.run(["docker", "--version"], capture_output=True, check=True)
111
+ except (subprocess.CalledProcessError, FileNotFoundError):
112
+ click.echo("āŒ Docker is not installed or not available in PATH")
113
+ raise click.Abort()
114
+
115
+ # Check if docker-compose or docker compose is available
116
+ compose_cmd = None
117
+ try:
118
+ subprocess.run(["docker", "compose", "version"], capture_output=True, check=True)
119
+ compose_cmd = ["docker", "compose"]
120
+ except (subprocess.CalledProcessError, FileNotFoundError):
121
+ try:
122
+ subprocess.run(["docker-compose", "version"], capture_output=True, check=True)
123
+ compose_cmd = ["docker-compose"]
124
+ except (subprocess.CalledProcessError, FileNotFoundError):
125
+ click.echo("āŒ Docker Compose is not installed")
126
+ raise click.Abort()
127
+
128
+ # Start the container
129
+ click.echo("šŸ“¦ Starting PostgreSQL container...")
130
+ cmd = compose_cmd + ["up"]
131
+ if detach:
132
+ cmd.append("-d")
133
+
134
+ result = subprocess.run(cmd, cwd=tmpdir)
135
+
136
+ if result.returncode != 0:
137
+ click.echo("āŒ Failed to start PostgreSQL container")
138
+ raise click.Abort()
139
+
140
+ if detach:
141
+ # Wait for PostgreSQL to be ready
142
+ click.echo("ā³ Waiting for PostgreSQL to be ready...")
143
+ for i in range(30):
144
+ result = subprocess.run(
145
+ ["docker", "exec", "narrator-postgres", "pg_isready", "-U", db_user],
146
+ capture_output=True
147
+ )
148
+ if result.returncode == 0:
149
+ click.echo("āœ… PostgreSQL is ready!")
150
+ click.echo(f"\nšŸŽ‰ Database available at:")
151
+ click.echo(f" postgresql+asyncpg://{db_user}:{db_password}@localhost:{db_port}/{db_name}")
152
+ return
153
+ time.sleep(1)
154
+
155
+ click.echo("āŒ PostgreSQL failed to start after 30 seconds")
156
+ raise click.Abort()
157
+
158
+ @main.command()
159
+ @click.option('--remove-volumes', is_flag=True, help='Remove data volumes (destroys all data)')
160
+ def docker_stop(remove_volumes):
161
+ """Stop the PostgreSQL container"""
162
+ # Check if docker is available
163
+ try:
164
+ subprocess.run(["docker", "--version"], capture_output=True, check=True)
165
+ except (subprocess.CalledProcessError, FileNotFoundError):
166
+ click.echo("āŒ Docker is not installed or not available in PATH")
167
+ raise click.Abort()
168
+
169
+ # Check if container exists
170
+ result = subprocess.run(
171
+ ["docker", "ps", "-a", "--format", "{{.Names}}"],
172
+ capture_output=True,
173
+ text=True
174
+ )
175
+
176
+ if "narrator-postgres" not in result.stdout:
177
+ click.echo("ā„¹ļø No Narrator PostgreSQL container found")
178
+ return
179
+
180
+ click.echo("šŸ›‘ Stopping PostgreSQL container...")
181
+
182
+ # Stop the container
183
+ subprocess.run(["docker", "stop", "narrator-postgres"], check=False)
184
+ subprocess.run(["docker", "rm", "narrator-postgres"], check=False)
185
+
186
+ if remove_volumes:
187
+ click.echo("šŸ—‘ļø Removing data volume...")
188
+ subprocess.run(["docker", "volume", "rm", "narrator_postgres_data"], check=False)
189
+ click.echo("āœ… Container and data removed")
190
+ else:
191
+ click.echo("āœ… Container stopped (data preserved)")
192
+
193
+ @main.command()
194
+ @click.option('--port', help='Port to expose PostgreSQL on (default: 5432 or NARRATOR_DB_PORT)')
195
+ def docker_setup(port):
196
+ """One-command Docker setup: start PostgreSQL and initialize tables"""
197
+ # Start PostgreSQL
198
+ ctx = click.get_current_context()
199
+ ctx.invoke(docker_start, port=port, detach=True)
200
+
201
+ # Get database configuration from environment or defaults
202
+ db_name = os.environ.get('NARRATOR_DB_NAME', 'narrator')
203
+ db_user = os.environ.get('NARRATOR_DB_USER', 'narrator')
204
+ db_password = os.environ.get('NARRATOR_DB_PASSWORD', 'narrator_dev')
205
+ db_port = port or os.environ.get('NARRATOR_DB_PORT', '5432')
206
+
207
+ # Set up database URL
208
+ database_url = f"postgresql+asyncpg://{db_user}:{db_password}@localhost:{db_port}/{db_name}"
209
+ os.environ['NARRATOR_DATABASE_URL'] = database_url
210
+
211
+ # Initialize tables
212
+ click.echo("\nšŸ”§ Initializing database tables...")
213
+ ctx.invoke(init, database_url=database_url)
214
+
215
+ click.echo("\nšŸŽ‰ Setup complete! Your database is ready.")
216
+ click.echo("\nTo use in your code:")
217
+ click.echo(f'export NARRATOR_DATABASE_URL="{database_url}"')
218
+ click.echo("\nTo stop the container: narrator docker-stop")
219
+ click.echo("To remove all data: narrator docker-stop --remove-volumes")
220
+
221
+ if __name__ == '__main__':
222
+ main()
@@ -0,0 +1,6 @@
1
+ """
2
+ Database migrations for Tyler Stores
3
+
4
+ This directory will contain future database migrations.
5
+ For new installations, the schema is created directly from the models.
6
+ """
@@ -0,0 +1,70 @@
1
+ import json
2
+ from sqlalchemy.types import TypeDecorator, TEXT, JSON
3
+
4
+ """Database models for SQLAlchemy"""
5
+ from sqlalchemy import Column, String, DateTime, Text, ForeignKey, Integer
6
+ from sqlalchemy.dialects.postgresql import JSONB
7
+ from sqlalchemy.orm import declarative_base
8
+ from sqlalchemy.orm import relationship
9
+ from datetime import datetime, UTC
10
+
11
+ class JSONBCompat(TypeDecorator):
12
+ impl = TEXT
13
+ cache_ok = True
14
+
15
+ def load_dialect_impl(self, dialect):
16
+ if dialect.name == 'postgresql':
17
+ return dialect.type_descriptor(JSONB())
18
+ else:
19
+ return dialect.type_descriptor(JSON())
20
+
21
+ def process_bind_param(self, value, dialect):
22
+ if dialect.name == 'postgresql':
23
+ return value
24
+ if value is not None:
25
+ return value
26
+ return value
27
+
28
+ def process_result_value(self, value, dialect):
29
+ if dialect.name == 'postgresql':
30
+ return value
31
+ if value is not None:
32
+ return value
33
+ return value
34
+
35
+ Base = declarative_base()
36
+
37
+ class ThreadRecord(Base):
38
+ __tablename__ = 'threads'
39
+
40
+ id = Column(String, primary_key=True)
41
+ title = Column(String, nullable=True)
42
+ attributes = Column(JSONBCompat, nullable=False, default={})
43
+ platforms = Column(JSONBCompat, nullable=True)
44
+ created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
45
+ updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
46
+
47
+ messages = relationship("MessageRecord", back_populates="thread", cascade="all, delete-orphan")
48
+
49
+ class MessageRecord(Base):
50
+ __tablename__ = 'messages'
51
+
52
+ id = Column(String, primary_key=True)
53
+ thread_id = Column(String, ForeignKey('threads.id', ondelete='CASCADE'), nullable=False)
54
+ sequence = Column(Integer, nullable=False)
55
+ turn = Column(Integer, nullable=True)
56
+ role = Column(String, nullable=False)
57
+ content = Column(Text, nullable=True)
58
+ reasoning_content = Column(Text, nullable=True)
59
+ name = Column(String, nullable=True)
60
+ tool_call_id = Column(String, nullable=True)
61
+ tool_calls = Column(JSONBCompat, nullable=True)
62
+ attributes = Column(JSONBCompat, nullable=False, default={})
63
+ timestamp = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
64
+ source = Column(JSONBCompat, nullable=True)
65
+ platforms = Column(JSONBCompat, nullable=True)
66
+ attachments = Column(JSONBCompat, nullable=True)
67
+ metrics = Column(JSONBCompat, nullable=False, default={})
68
+ reactions = Column(JSONBCompat, nullable=True)
69
+
70
+ thread = relationship("ThreadRecord", back_populates="messages")