bizy-ai 1.0.1__py3-none-any.whl → 1.1.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.
agent/tasks.py CHANGED
@@ -233,19 +233,104 @@ class TaskManager:
233
233
  }
234
234
 
235
235
  def get_task_velocity(self, days=30):
236
- """Calculate average tasks completed per day over a period"""
237
- start_date = datetime.now() - timedelta(days=days)
238
-
239
- logs = self.session.query(DailyLog).filter(
240
- DailyLog.date >= start_date
241
- ).all()
242
-
243
- if not logs:
244
- return 0
245
-
246
- total_completed = sum(log.tasks_completed for log in logs)
247
- return total_completed / len(logs) if logs else 0
248
-
236
+ """
237
+ Calculate average tasks completed per day over a period.
238
+ Uses actual completed_at timestamps for accurate velocity calculation.
239
+ """
240
+ # Get tasks completed in the specified period
241
+ completed_tasks = self.get_completed_tasks_this_week(days=days)
242
+
243
+ if not completed_tasks:
244
+ return 0.0
245
+
246
+ # Calculate velocity as tasks per day
247
+ velocity = len(completed_tasks) / days
248
+ return velocity
249
+
250
+ def get_completed_tasks_this_week(self, days=7):
251
+ """Get tasks completed in the last N days based on completed_at timestamp"""
252
+ # Use UTC time to match database timestamps
253
+ now = datetime.utcnow()
254
+ start_date = now - timedelta(days=days)
255
+
256
+ completed_tasks = self.session.query(Task).filter(
257
+ and_(
258
+ Task.status == 'completed',
259
+ Task.completed_at >= start_date,
260
+ Task.completed_at <= now
261
+ )
262
+ ).order_by(Task.completed_at.desc()).all()
263
+
264
+ return completed_tasks
265
+
266
+ def get_created_tasks_this_week(self, days=7):
267
+ """Get tasks created in the last N days based on created_at timestamp"""
268
+ # Use UTC time to match database timestamps
269
+ now = datetime.utcnow()
270
+ start_date = now - timedelta(days=days)
271
+
272
+ created_tasks = self.session.query(Task).filter(
273
+ and_(
274
+ Task.created_at >= start_date,
275
+ Task.created_at <= now
276
+ )
277
+ ).order_by(Task.created_at.desc()).all()
278
+
279
+ return created_tasks
280
+
281
+ def get_weekly_task_stats(self, days=7):
282
+ """
283
+ Get weekly statistics based on actual task completion dates (completed_at).
284
+ This is more accurate than DailyLog-based stats as it reflects actual work completed.
285
+ """
286
+ completed_tasks = self.get_completed_tasks_this_week(days)
287
+ created_tasks = self.get_created_tasks_this_week(days)
288
+
289
+ # Calculate statistics
290
+ tasks_completed = len(completed_tasks)
291
+ tasks_created = len(created_tasks)
292
+
293
+ # Calculate completion rate (completed vs created this week)
294
+ completion_rate = (tasks_completed / tasks_created * 100) if tasks_created > 0 else 0
295
+
296
+ # Calculate total estimated hours
297
+ total_estimated_hours = sum(
298
+ task.estimated_hours or 0 for task in completed_tasks
299
+ )
300
+
301
+ # Calculate total actual hours
302
+ total_actual_hours = sum(
303
+ task.actual_hours or 0 for task in completed_tasks
304
+ )
305
+
306
+ # Break down by category
307
+ categories = {}
308
+ for task in completed_tasks:
309
+ category = task.category or 'uncategorized'
310
+ if category not in categories:
311
+ categories[category] = 0
312
+ categories[category] += 1
313
+
314
+ # Break down by priority
315
+ priorities = {}
316
+ for task in completed_tasks:
317
+ priority = task.priority
318
+ if priority not in priorities:
319
+ priorities[priority] = 0
320
+ priorities[priority] += 1
321
+
322
+ return {
323
+ 'tasks_completed_this_week': tasks_completed,
324
+ 'tasks_created_this_week': tasks_created,
325
+ 'completion_rate': completion_rate,
326
+ 'total_estimated_hours': total_estimated_hours,
327
+ 'total_actual_hours': total_actual_hours,
328
+ 'completed_tasks': [task.to_dict() for task in completed_tasks],
329
+ 'categories': categories,
330
+ 'priorities': priorities,
331
+ 'period_days': days
332
+ }
333
+
249
334
  def close(self):
250
335
  """Close the database session"""
251
336
  self.session.close()
agent/weekly_review.py CHANGED
@@ -25,22 +25,22 @@ def run_weekly_review():
25
25
  task_mgr = TaskManager()
26
26
  planner = BusinessPlanner()
27
27
 
28
- # Get weekly statistics
29
- weekly_stats = task_mgr.get_weekly_stats()
30
-
28
+ # Get weekly statistics based on actual task completions
29
+ weekly_stats = task_mgr.get_weekly_task_stats()
30
+
31
31
  # Get goal progress
32
32
  active_goals = planner.get_active_goals()
33
33
  goals_progress = "\n".join([
34
34
  f"- {g.title}: {g.progress_percentage:.0f}% complete"
35
35
  for g in active_goals[:5]
36
36
  ]) if active_goals else "No active goals"
37
-
38
- # Get key events
39
- key_events = []
40
- for log in weekly_stats.get('logs', []):
41
- if log.get('wins'):
42
- key_events.append(f"Win: {log['wins']}")
43
- key_events_str = "\n".join(key_events[-10:]) if key_events else "No events logged"
37
+
38
+ # Get key events from completed tasks
39
+ completed_tasks = weekly_stats.get('completed_tasks', [])
40
+ key_events_str = "\n".join([
41
+ f"✓ {task['title']}" + (f" ({task['category']})" if task['category'] else "")
42
+ for task in completed_tasks[:10]
43
+ ]) if completed_tasks else "No tasks completed this week"
44
44
 
45
45
  # Generate review
46
46
  console.print("[dim]Generating comprehensive analysis...[/dim]\n")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bizy-ai
3
- Version: 1.0.1
3
+ Version: 1.1.0
4
4
  Summary: AI-powered business planning and execution agent
5
5
  Author: Reid Chatham
6
6
  License: MIT
@@ -37,6 +37,13 @@ Requires-Dist: rich>=13.0.0
37
37
  Requires-Dist: python-dateutil>=2.8.2
38
38
  Requires-Dist: click>=8.1.0
39
39
  Requires-Dist: jinja2>=3.1.2
40
+ Provides-Extra: dev
41
+ Requires-Dist: pytest>=7.4.0; extra == "dev"
42
+ Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
43
+ Requires-Dist: pytest-mock>=3.12.0; extra == "dev"
44
+ Requires-Dist: pytest-watch>=4.2.0; extra == "dev"
45
+ Requires-Dist: black>=23.0.0; extra == "dev"
46
+ Requires-Dist: flake8>=6.1.0; extra == "dev"
40
47
  Dynamic: license-file
41
48
  Dynamic: requires-python
42
49
 
@@ -88,7 +95,50 @@ pip install -e .
88
95
  ./setup.sh
89
96
  ```
90
97
 
91
- See **[INSTALL.md](INSTALL.md)** for detailed installation options.
98
+ See **[INSTALL.md](docs/INSTALL.md)** for detailed installation options.
99
+
100
+ ---
101
+
102
+ ## Development
103
+
104
+ ### Test-Driven Development (TDD)
105
+
106
+ This project follows TDD principles. All new features must:
107
+
108
+ 1. **Write tests first** - Define expected behavior
109
+ 2. **Run tests (they should fail)** - Red phase
110
+ 3. **Implement feature** - Green phase
111
+ 4. **Refactor** - Clean up code
112
+
113
+ ### Running Tests
114
+
115
+ ```bash
116
+ # Run all tests with coverage
117
+ make test
118
+
119
+ # Run tests in watch mode
120
+ make test-watch
121
+
122
+ # Run specific test file
123
+ pytest tests/test_tasks.py -v
124
+ ```
125
+
126
+ ### Database Environments
127
+
128
+ - **Production**: `~/.business-agent/tasks.db` - Your actual business data 🔒
129
+ - **Development**: `~/.business-agent/dev_tasks.db` - Safe for experimentation ⚙️
130
+ - **Test**: In-memory - Isolated, clean for each test 🧪
131
+
132
+ ```bash
133
+ # Use development database
134
+ export BIZY_ENV=development
135
+ make dev
136
+
137
+ # Use production database (default)
138
+ export BIZY_ENV=production
139
+ ```
140
+
141
+ See **[CONTRIBUTING.md](CONTRIBUTING.md)** for detailed development guidelines.
92
142
 
93
143
  ---
94
144
 
@@ -0,0 +1,21 @@
1
+ agent/__init__.py,sha256=pHeIDi-ibuwZqaNqxdIDRvzedWtPywssbaYIDawMcvQ,88
2
+ agent/cli.py,sha256=J-TlTArbSD2eHkn93Kt7Y7YXZb84NJPGj3XDRgUPsZw,15186
3
+ agent/core.py,sha256=UILN_DjsSr91KN3YyQaD_cZgF9vW8m827-W0pZMTUnM,6561
4
+ agent/evening_review.py,sha256=Y4d-t62zfaufM8mnSvkhBBZ0wMw1R8jtj4cN8r30t4E,4929
5
+ agent/models.py,sha256=Et7GDIW8E-S444j0AbqoFrvNXL4hJ9iWPM989CS34E0,8727
6
+ agent/morning_brief.py,sha256=zcu4nUOnIQtoZTiK1XlEjuudiUAABrps3PvAwt2Avmk,4732
7
+ agent/plan_manager.py,sha256=7ipJBW-QS308dSpULjJGcDPjsXKA-yVh1mljFejCv_U,12225
8
+ agent/planner.py,sha256=-KlyZ-2jMQTUy0QVZCH_NuubENU-UrPXVhat3ju54ew,13114
9
+ agent/research.py,sha256=HvqGHENQ0v1lJadGYNuf3eb6fZoR6qzzIPzqcsnhFSE,6429
10
+ agent/tasks.py,sha256=PzisO_2a7Hgplxf3CxnGYLG-Fg8O9ylHYC5193cHyZo,12155
11
+ agent/weekly_review.py,sha256=83fYwe3hKEhqAQfMlj7WmvvMhTNHtUnqr1PFXCHRGWo,2394
12
+ bizy_ai-1.1.0.dist-info/licenses/LICENSE,sha256=__BSNgmbeWQ1IA57XKyGhQajUNcF-pZjvBAY268fCWM,1069
13
+ scripts/init_db.py,sha256=lF1rJAuaeOX19dYQKURzePYWmjkjLPH_4L0D2OiRgQA,3376
14
+ scripts/load_business_plan.py,sha256=gb4_NPL39PX5yie7zENYgViPBoTqHaLYbTGXPdZU9wM,3560
15
+ scripts/migrate_db.py,sha256=b51nlLn62nThDGRNqPbxT3jNM2Zdg56hKyymYFTcrUk,1220
16
+ scripts/scheduler.py,sha256=4LDL7Mb8ZWeOFpy6i8LuMff4HWjFwm10XrcMpLkZmUY,3459
17
+ bizy_ai-1.1.0.dist-info/METADATA,sha256=9zDRI0w3sQuFbGFPFbZC97Gq0kY-vbeyFMjk99OWjxo,13726
18
+ bizy_ai-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ bizy_ai-1.1.0.dist-info/entry_points.txt,sha256=yDZc2xFUlCOPuHtAaNissB16AZFzOnOL8xeStkDujAg,39
20
+ bizy_ai-1.1.0.dist-info/top_level.txt,sha256=k5ce4bNe_tK9tse1lxY4b8nPSipbtgoA28GHmM2ojwk,14
21
+ bizy_ai-1.1.0.dist-info/RECORD,,
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env python3
2
+ """Script to load business plan from YAML into the database"""
3
+
4
+ import yaml
5
+ import sys
6
+ import os
7
+ from datetime import datetime
8
+ from agent.models import get_session, BusinessPlan
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+ from rich.prompt import Prompt
12
+
13
+ console = Console()
14
+
15
+ def load_business_plan(file_path=None, plan_name=None):
16
+ """Load a business plan from YAML into database"""
17
+
18
+ console.print()
19
+
20
+ # If no file path provided, request one
21
+ if not file_path:
22
+ file_path = Prompt.ask("📁 [bold]Enter path to business plan YAML file[/bold]")
23
+
24
+ # Expand user path if needed
25
+ file_path = os.path.expanduser(file_path)
26
+
27
+ # Check if file exists
28
+ if not os.path.exists(file_path):
29
+ console.print(f"[bold red]❌ Error: File not found: {file_path}[/bold red]")
30
+ return False
31
+
32
+ # Read the YAML file
33
+ try:
34
+ with open(file_path, 'r') as f:
35
+ file_content = f.read()
36
+ plan_data = yaml.safe_load(file_content)
37
+ except Exception as e:
38
+ console.print(f"[bold red]❌ Error reading YAML file: {e}[/bold red]")
39
+ return False
40
+
41
+ session = get_session()
42
+
43
+ try:
44
+ # Get plan name: CLI option > YAML 'name' field > first # header > prompt user
45
+ if not plan_name:
46
+ # Try to get name from YAML 'name' field
47
+ plan_name = plan_data.get('name')
48
+
49
+ if not plan_name:
50
+ # Try to extract from first # header in file
51
+ for line in file_content.split('\n'):
52
+ line = line.strip()
53
+ if line.startswith('# ') and not line.startswith('##'):
54
+ plan_name = line[2:].strip()
55
+ break
56
+
57
+ if not plan_name:
58
+ # Prompt user for name
59
+ plan_name = Prompt.ask(f"📝 [bold]Enter business plan name[/bold]")
60
+
61
+ # Deactivate any existing active plans
62
+ active_plans = session.query(BusinessPlan).filter_by(is_active=True).all()
63
+ for plan in active_plans:
64
+ plan.is_active = False
65
+ console.print(f"[yellow]⚠ Deactivated existing plan: {plan.name}[/yellow]")
66
+
67
+ # Create new business plan
68
+ new_plan = BusinessPlan(
69
+ name=plan_name,
70
+ version=plan_data.get('version', '1.0'),
71
+ vision=plan_data.get('vision', ''),
72
+ mission=plan_data.get('mission', ''),
73
+ value_proposition=plan_data.get('value_proposition', ''),
74
+ target_market=plan_data.get('target_market', ''),
75
+ revenue_model=plan_data.get('revenue_model', ''),
76
+ is_active=True
77
+ )
78
+
79
+ session.add(new_plan)
80
+ session.commit()
81
+
82
+ console.print(Panel(
83
+ f"[green]✅ Business Plan Loaded Successfully[/green]\n\n"
84
+ f"Name: {new_plan.name}\n"
85
+ f"Version: {new_plan.version}\n"
86
+ f"ID: {new_plan.id}\n"
87
+ f"Created: {new_plan.created_at.strftime('%Y-%m-%d %H:%M:%S')}\n"
88
+ f"Active: {'Yes' if new_plan.is_active else 'No'}",
89
+ title="Success",
90
+ border_style="green"
91
+ ))
92
+
93
+ console.print()
94
+ return True
95
+
96
+ except Exception as e:
97
+ console.print(f"[bold red]❌ Error creating business plan: {e}[/bold red]")
98
+ session.rollback()
99
+ return False
100
+ finally:
101
+ session.close()
102
+
103
+ if __name__ == "__main__":
104
+ # Check for command line argument
105
+ if len(sys.argv) > 1:
106
+ file_path = sys.argv[1]
107
+ else:
108
+ file_path = None
109
+
110
+ load_business_plan(file_path)
scripts/migrate_db.py ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env python3
2
+ """Migrate database to add name and updated_at columns to business_plans table"""
3
+
4
+ import sqlite3
5
+ import os
6
+
7
+ db_path = os.path.expanduser('~/.business-agent/tasks.db')
8
+
9
+ print(f"Migrating database at: {db_path}")
10
+
11
+ conn = sqlite3.connect(db_path)
12
+ cursor = conn.cursor()
13
+
14
+ try:
15
+ # Check if name column exists
16
+ cursor.execute("PRAGMA table_info(business_plans)")
17
+ columns = [row[1] for row in cursor.fetchall()]
18
+
19
+ if 'name' not in columns:
20
+ print("Adding 'name' column to business_plans table...")
21
+ cursor.execute("ALTER TABLE business_plans ADD COLUMN name VARCHAR(255)")
22
+ print("✅ Added 'name' column")
23
+ else:
24
+ print("✓ 'name' column already exists")
25
+
26
+ if 'updated_at' not in columns:
27
+ print("Adding 'updated_at' column to business_plans table...")
28
+ cursor.execute("ALTER TABLE business_plans ADD COLUMN updated_at DATETIME")
29
+ print("✅ Added 'updated_at' column")
30
+ else:
31
+ print("✓ 'updated_at' column already exists")
32
+
33
+ conn.commit()
34
+ print("\n✅ Database migration completed successfully!")
35
+
36
+ except Exception as e:
37
+ print(f"❌ Error during migration: {e}")
38
+ conn.rollback()
39
+ finally:
40
+ conn.close()
scripts/scheduler.py ADDED
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Business Agent Main Scheduler
4
+ Runs automated daily tasks on schedule
5
+ """
6
+
7
+ import schedule
8
+ import time
9
+ import subprocess
10
+ import sys
11
+ from datetime import datetime
12
+ from rich.console import Console
13
+ from dotenv import load_dotenv
14
+
15
+ load_dotenv()
16
+
17
+ console = Console()
18
+
19
+ def run_script(script_name, description):
20
+ """Run a Python script and handle errors"""
21
+ console.print(f"\n[bold cyan]Running {description}...[/bold cyan]")
22
+ console.print(f"[dim]{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}[/dim]\n")
23
+
24
+ try:
25
+ result = subprocess.run(
26
+ [sys.executable, f"scripts/{script_name}"],
27
+ capture_output=False,
28
+ text=True
29
+ )
30
+
31
+ if result.returncode == 0:
32
+ console.print(f"[green]✓ {description} completed successfully[/green]\n")
33
+ else:
34
+ console.print(f"[yellow]⚠ {description} finished with warnings[/yellow]\n")
35
+
36
+ except Exception as e:
37
+ console.print(f"[red]✗ Error running {description}: {e}[/red]\n")
38
+
39
+ def run_morning_brief():
40
+ """Run morning briefing"""
41
+ run_script("morning_brief.py", "Morning Briefing")
42
+
43
+ def run_evening_review():
44
+ """Run evening review"""
45
+ run_script("evening_review.py", "Evening Review")
46
+
47
+ def run_weekly_review():
48
+ """Run weekly review"""
49
+ run_script("weekly_review.py", "Weekly Review")
50
+
51
+ def main():
52
+ console.print("\n[bold cyan]═══════════════════════════════════════════[/bold cyan]")
53
+ console.print("[bold cyan] 🤖 Business Agent Scheduler Started[/bold cyan]")
54
+ console.print("[bold cyan]═══════════════════════════════════════════[/bold cyan]\n")
55
+
56
+ # Schedule jobs
57
+ # Morning briefing: Monday-Friday at 8:00 AM
58
+ schedule.every().monday.at("08:00").do(run_morning_brief)
59
+ schedule.every().tuesday.at("08:00").do(run_morning_brief)
60
+ schedule.every().wednesday.at("08:00").do(run_morning_brief)
61
+ schedule.every().thursday.at("08:00").do(run_morning_brief)
62
+ schedule.every().friday.at("08:00").do(run_morning_brief)
63
+
64
+ # Evening review: Monday-Friday at 6:00 PM
65
+ schedule.every().monday.at("18:00").do(run_evening_review)
66
+ schedule.every().tuesday.at("18:00").do(run_evening_review)
67
+ schedule.every().wednesday.at("18:00").do(run_evening_review)
68
+ schedule.every().thursday.at("18:00").do(run_evening_review)
69
+ schedule.every().friday.at("18:00").do(run_evening_review)
70
+
71
+ # Weekly review: Sunday at 7:00 PM
72
+ schedule.every().sunday.at("19:00").do(run_weekly_review)
73
+
74
+ console.print("[bold]📅 Scheduled Tasks:[/bold]")
75
+ console.print(" • Morning Briefings: Mon-Fri at 8:00 AM")
76
+ console.print(" • Evening Reviews: Mon-Fri at 6:00 PM")
77
+ console.print(" • Weekly Review: Sunday at 7:00 PM")
78
+ console.print()
79
+ console.print("[dim]Press Ctrl+C to stop the scheduler[/dim]")
80
+ console.print()
81
+
82
+ # Keep the scheduler running
83
+ try:
84
+ while True:
85
+ schedule.run_pending()
86
+ time.sleep(60) # Check every minute
87
+
88
+ except KeyboardInterrupt:
89
+ console.print("\n\n[yellow]Scheduler stopped by user[/yellow]")
90
+ console.print("[dim]All scheduled tasks have been cancelled[/dim]\n")
91
+ sys.exit(0)
92
+
93
+ if __name__ == "__main__":
94
+ main()
@@ -1,18 +0,0 @@
1
- agent/__init__.py,sha256=pHeIDi-ibuwZqaNqxdIDRvzedWtPywssbaYIDawMcvQ,88
2
- agent/cli.py,sha256=8raEuVh3rEDRQS8QGROu_ONiGdEPAwZEP2-gshrb3mA,7264
3
- agent/core.py,sha256=UILN_DjsSr91KN3YyQaD_cZgF9vW8m827-W0pZMTUnM,6561
4
- agent/evening_review.py,sha256=Y4d-t62zfaufM8mnSvkhBBZ0wMw1R8jtj4cN8r30t4E,4929
5
- agent/models.py,sha256=6bDSLXRxztF50J0AoTi5PX3ckf74oPhyQXfOccfVGdk,7937
6
- agent/morning_brief.py,sha256=zcu4nUOnIQtoZTiK1XlEjuudiUAABrps3PvAwt2Avmk,4732
7
- agent/planner.py,sha256=-KlyZ-2jMQTUy0QVZCH_NuubENU-UrPXVhat3ju54ew,13114
8
- agent/research.py,sha256=HvqGHENQ0v1lJadGYNuf3eb6fZoR6qzzIPzqcsnhFSE,6429
9
- agent/tasks.py,sha256=PaQzRcnzp5HPWVlQKG3KLwdNBNqHTsqCuoGmRwRh8dA,9041
10
- agent/weekly_review.py,sha256=ljwy0Aq6yFE_gs8TQjJKCO9Ob59fXsu8L_gDPiRQdmc,2298
11
- bizy_ai-1.0.1.dist-info/licenses/LICENSE,sha256=__BSNgmbeWQ1IA57XKyGhQajUNcF-pZjvBAY268fCWM,1069
12
- scripts/agent_cli.py,sha256=sG-iRmFZCzm5SkqDtVV1KzZ293SEtvFpY8A1_b69dJU,6971
13
- scripts/init_db.py,sha256=lF1rJAuaeOX19dYQKURzePYWmjkjLPH_4L0D2OiRgQA,3376
14
- bizy_ai-1.0.1.dist-info/METADATA,sha256=YLRhUVVs_Ve3a5wXC6aIt3etvyJaWW2-NZKY8HLKtoo,12480
15
- bizy_ai-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
- bizy_ai-1.0.1.dist-info/entry_points.txt,sha256=yDZc2xFUlCOPuHtAaNissB16AZFzOnOL8xeStkDujAg,39
17
- bizy_ai-1.0.1.dist-info/top_level.txt,sha256=k5ce4bNe_tK9tse1lxY4b8nPSipbtgoA28GHmM2ojwk,14
18
- bizy_ai-1.0.1.dist-info/RECORD,,