Spaces:
Sleeping
Implement Phase 1: Persona-based LLM query system for urban planning
Browse filesThis commit implements the complete Phase 1 of the AI Personas system,
enabling users to query synthetic personas representing diverse urban
planning stakeholders and receive contextually-aware responses.
Core Features:
- 6 diverse synthetic personas (urban planner, business owner, engineer,
resident, housing advocate, developer)
- Comprehensive persona data models with demographics, psychographics,
and behavioral profiles
- Environmental context system modeling built environment, social,
economic, and temporal factors
- Anthropic Claude LLM integration with smart prompt construction
- End-to-end query-response pipeline
- Interactive CLI and example scripts
- Full test suite and documentation
Key Components:
- src/personas: Persona data models and database management
- src/context: Environmental context system
- src/llm: LLM integration (Anthropic Claude, extensible to Be.FM)
- src/pipeline: Query orchestration and response handling
- data/personas: 6 detailed persona JSON definitions
- data/contexts: Sample downtown district context
- examples: Usage demonstrations
- docs: Comprehensive getting started guide
Technical Stack:
- Python 3.11+
- Pydantic for data validation
- Anthropic Claude API
- Modular architecture for Phase 2/3 extensions
Testing:
- All personas load correctly (6/6)
- Context system functional
- Search and filtering operational
- Summary generation working
Next Steps:
- Phase 2: Population-based response distributions
- Phase 3: Multi-persona influence and opinion equilibria
π€ Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- .env.example +23 -0
- .gitignore +60 -0
- README.md +148 -0
- data/contexts/downtown_district.json +95 -0
- data/personas/david_kim.json +98 -0
- data/personas/elena_rodriguez.json +97 -0
- data/personas/james_obrien.json +92 -0
- data/personas/marcus_thompson.json +90 -0
- data/personas/priya_patel.json +99 -0
- data/personas/sarah_chen.json +90 -0
- docs/GETTING_STARTED.md +247 -0
- docs/PHASE1_SUMMARY.md +251 -0
- examples/phase1_multiple_perspectives.py +81 -0
- examples/phase1_simple_query.py +54 -0
- requirements.txt +38 -0
- src/__init__.py +3 -0
- src/cli.py +244 -0
- src/context/__init__.py +19 -0
- src/context/database.py +107 -0
- src/context/models.py +270 -0
- src/dynamics/__init__.py +11 -0
- src/llm/__init__.py +6 -0
- src/llm/anthropic_client.py +135 -0
- src/llm/prompt_builder.py +148 -0
- src/personas/__init__.py +12 -0
- src/personas/database.py +206 -0
- src/personas/models.py +213 -0
- src/pipeline/__init__.py +5 -0
- src/pipeline/query_engine.py +228 -0
- src/population/__init__.py +10 -0
- tests/test_basic_functionality.py +173 -0
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Anthropic API Configuration
|
| 2 |
+
ANTHROPIC_API_KEY=your_api_key_here
|
| 3 |
+
|
| 4 |
+
# Model Configuration
|
| 5 |
+
LLM_MODEL=claude-3-5-sonnet-20241022
|
| 6 |
+
LLM_MAX_TOKENS=2048
|
| 7 |
+
LLM_TEMPERATURE=0.7
|
| 8 |
+
|
| 9 |
+
# Database Configuration
|
| 10 |
+
DATABASE_URL=sqlite:///./data/personas.db
|
| 11 |
+
|
| 12 |
+
# Application Settings
|
| 13 |
+
DEBUG=True
|
| 14 |
+
LOG_LEVEL=INFO
|
| 15 |
+
|
| 16 |
+
# Phase 2: Population Sampling
|
| 17 |
+
DEFAULT_POPULATION_SIZE=100
|
| 18 |
+
SAMPLING_METHOD=gaussian # gaussian, uniform, bootstrap
|
| 19 |
+
|
| 20 |
+
# Phase 3: Social Dynamics
|
| 21 |
+
MAX_ITERATIONS=100
|
| 22 |
+
CONVERGENCE_THRESHOLD=0.001
|
| 23 |
+
INFLUENCE_MODEL=degroot # degroot, bounded_confidence, voter
|
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
build/
|
| 8 |
+
develop-eggs/
|
| 9 |
+
dist/
|
| 10 |
+
downloads/
|
| 11 |
+
eggs/
|
| 12 |
+
.eggs/
|
| 13 |
+
lib/
|
| 14 |
+
lib64/
|
| 15 |
+
parts/
|
| 16 |
+
sdist/
|
| 17 |
+
var/
|
| 18 |
+
wheels/
|
| 19 |
+
*.egg-info/
|
| 20 |
+
.installed.cfg
|
| 21 |
+
*.egg
|
| 22 |
+
|
| 23 |
+
# Virtual environments
|
| 24 |
+
venv/
|
| 25 |
+
ENV/
|
| 26 |
+
env/
|
| 27 |
+
.venv
|
| 28 |
+
|
| 29 |
+
# Environment variables
|
| 30 |
+
.env
|
| 31 |
+
|
| 32 |
+
# IDE
|
| 33 |
+
.vscode/
|
| 34 |
+
.idea/
|
| 35 |
+
*.swp
|
| 36 |
+
*.swo
|
| 37 |
+
*~
|
| 38 |
+
|
| 39 |
+
# Testing
|
| 40 |
+
.pytest_cache/
|
| 41 |
+
.coverage
|
| 42 |
+
htmlcov/
|
| 43 |
+
.tox/
|
| 44 |
+
|
| 45 |
+
# Database
|
| 46 |
+
*.db
|
| 47 |
+
*.sqlite
|
| 48 |
+
*.sqlite3
|
| 49 |
+
|
| 50 |
+
# Logs
|
| 51 |
+
*.log
|
| 52 |
+
|
| 53 |
+
# OS
|
| 54 |
+
.DS_Store
|
| 55 |
+
Thumbs.db
|
| 56 |
+
|
| 57 |
+
# Project specific
|
| 58 |
+
data/cache/
|
| 59 |
+
outputs/
|
| 60 |
+
experiments/
|
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AI Personas for Urban Planning
|
| 2 |
+
|
| 3 |
+
A multi-phase persona simulation system for urban planning and built environment design, enabling stakeholder perspective analysis and opinion dynamics modeling.
|
| 4 |
+
|
| 5 |
+
## Overview
|
| 6 |
+
|
| 7 |
+
This system allows users to:
|
| 8 |
+
- **Phase 1**: Query synthetic personas representing urban stakeholders and receive contextually-aware responses
|
| 9 |
+
- **Phase 2**: Generate response distributions from populations of persona variants
|
| 10 |
+
- **Phase 3**: Model multi-persona interactions to discover opinion equilibria
|
| 11 |
+
|
| 12 |
+
## Project Structure
|
| 13 |
+
|
| 14 |
+
```
|
| 15 |
+
AI_Personas/
|
| 16 |
+
βββ src/
|
| 17 |
+
β βββ personas/ # Persona data models and database
|
| 18 |
+
β βββ context/ # Environmental/built environment context
|
| 19 |
+
β βββ llm/ # LLM integration (Anthropic Claude, Be.FM later)
|
| 20 |
+
β βββ pipeline/ # Query-response pipeline
|
| 21 |
+
β βββ population/ # Phase 2: Population sampling and distributions
|
| 22 |
+
β βββ dynamics/ # Phase 3: Social influence and equilibrium
|
| 23 |
+
βββ data/
|
| 24 |
+
β βββ personas/ # Persona JSON definitions
|
| 25 |
+
β βββ contexts/ # Environmental context data
|
| 26 |
+
βββ tests/
|
| 27 |
+
βββ examples/ # Usage examples and demos
|
| 28 |
+
βββ docs/ # Additional documentation
|
| 29 |
+
βββ requirements.txt # Python dependencies
|
| 30 |
+
βββ .env.example # Environment variable template
|
| 31 |
+
βββ README.md
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
## Quick Start
|
| 35 |
+
|
| 36 |
+
### 1. Installation
|
| 37 |
+
|
| 38 |
+
```bash
|
| 39 |
+
# Create virtual environment
|
| 40 |
+
python -m venv venv
|
| 41 |
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
| 42 |
+
|
| 43 |
+
# Install dependencies
|
| 44 |
+
pip install -r requirements.txt
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
### 2. Configuration
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
# Copy environment template
|
| 51 |
+
cp .env.example .env
|
| 52 |
+
|
| 53 |
+
# Edit .env and add your Anthropic API key
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
### 3. Run Example
|
| 57 |
+
|
| 58 |
+
```bash
|
| 59 |
+
# Phase 1: Single persona query
|
| 60 |
+
python examples/phase1_single_query.py
|
| 61 |
+
|
| 62 |
+
# Interactive CLI
|
| 63 |
+
python -m src.cli
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
## Personas
|
| 67 |
+
|
| 68 |
+
The system includes 6 synthetic personas representing diverse urban planning stakeholders:
|
| 69 |
+
|
| 70 |
+
1. **Sarah Chen** - Urban Planner (30s, progressive, sustainability-focused)
|
| 71 |
+
2. **Marcus Thompson** - Local Business Owner (50s, pragmatic, economy-focused)
|
| 72 |
+
3. **Dr. Elena Rodriguez** - Transportation Engineer (40s, data-driven, efficiency-focused)
|
| 73 |
+
4. **James O'Brien** - Long-time Resident (65+, traditional, community-focused)
|
| 74 |
+
5. **Priya Patel** - Housing Advocate (20s, activist, equity-focused)
|
| 75 |
+
6. **David Kim** - Real Estate Developer (40s, market-driven, growth-focused)
|
| 76 |
+
|
| 77 |
+
## Phase Roadmap
|
| 78 |
+
|
| 79 |
+
### Phase 1: Single Persona Query-Response β (Current)
|
| 80 |
+
- Query individual personas with contextual awareness
|
| 81 |
+
- Anthropic Claude integration
|
| 82 |
+
- Environmental context system
|
| 83 |
+
|
| 84 |
+
### Phase 2: Population Response Distribution (Planned)
|
| 85 |
+
- Generate persona variants with statistical distributions
|
| 86 |
+
- Parallel querying of persona populations
|
| 87 |
+
- Response clustering and statistical analysis
|
| 88 |
+
- Distribution visualization
|
| 89 |
+
|
| 90 |
+
### Phase 3: Multi-Persona Influence & Equilibrium (Planned)
|
| 91 |
+
- Social network graph modeling
|
| 92 |
+
- Opinion dynamics simulation (DeGroot, Bounded Confidence)
|
| 93 |
+
- Equilibrium detection algorithms
|
| 94 |
+
- Interactive influence visualization
|
| 95 |
+
|
| 96 |
+
## Technology Stack
|
| 97 |
+
|
| 98 |
+
- **Python 3.11+**: Core language
|
| 99 |
+
- **FastAPI**: REST API layer
|
| 100 |
+
- **SQLite/PostgreSQL**: Persona and context storage
|
| 101 |
+
- **Anthropic Claude**: LLM (will support Be.FM from Stanford)
|
| 102 |
+
- **Pydantic**: Data validation
|
| 103 |
+
- **NumPy/SciPy**: Statistical analysis (Phase 2)
|
| 104 |
+
- **NetworkX**: Graph modeling (Phase 3)
|
| 105 |
+
|
| 106 |
+
## Usage Examples
|
| 107 |
+
|
| 108 |
+
### Query a Persona
|
| 109 |
+
|
| 110 |
+
```python
|
| 111 |
+
from src.pipeline.query_engine import QueryEngine
|
| 112 |
+
|
| 113 |
+
engine = QueryEngine()
|
| 114 |
+
|
| 115 |
+
# Ask a persona about a planning issue
|
| 116 |
+
response = engine.query(
|
| 117 |
+
persona_id="sarah_chen",
|
| 118 |
+
question="What do you think about the proposed bike lane on Main Street?",
|
| 119 |
+
context={
|
| 120 |
+
"location": "downtown_district",
|
| 121 |
+
"time": "rush_hour",
|
| 122 |
+
"recent_events": ["community_meeting_last_week"]
|
| 123 |
+
}
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
print(response)
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
### Get Population Distribution (Phase 2)
|
| 130 |
+
|
| 131 |
+
```python
|
| 132 |
+
from src.population.sampler import PopulationSampler
|
| 133 |
+
|
| 134 |
+
sampler = PopulationSampler(base_persona="sarah_chen", n_variants=100)
|
| 135 |
+
distribution = sampler.query_population(
|
| 136 |
+
question="Rate your support for the bike lane (1-10)"
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
# distribution.mean, distribution.std, distribution.histogram
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
## Contributing
|
| 143 |
+
|
| 144 |
+
This project is under active development. Contributions welcome!
|
| 145 |
+
|
| 146 |
+
## License
|
| 147 |
+
|
| 148 |
+
MIT License
|
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"context_id": "downtown_district",
|
| 3 |
+
"built_environment": {
|
| 4 |
+
"location_id": "downtown_district",
|
| 5 |
+
"name": "Downtown District",
|
| 6 |
+
"area_type": "downtown",
|
| 7 |
+
"population_density": 15000,
|
| 8 |
+
"housing_density": 8000,
|
| 9 |
+
"land_use_mix": 0.85,
|
| 10 |
+
"transit_access": "excellent",
|
| 11 |
+
"bike_infrastructure": 7,
|
| 12 |
+
"sidewalk_coverage": 0.98,
|
| 13 |
+
"parking_availability": 4,
|
| 14 |
+
"walkability_score": 92,
|
| 15 |
+
"parks_access": 6,
|
| 16 |
+
"retail_access": 10,
|
| 17 |
+
"building_age": "mixed (1920s-2020s)",
|
| 18 |
+
"historic_district": true,
|
| 19 |
+
"infrastructure_condition": 7,
|
| 20 |
+
"description": "Dense, mixed-use downtown core with historic buildings and new high-rises. Excellent pedestrian environment with wide sidewalks, street trees, and activated ground floors. Limited parking but excellent transit access. Home to restaurants, offices, cultural venues, and growing residential population."
|
| 21 |
+
},
|
| 22 |
+
"social_context": {
|
| 23 |
+
"location_id": "downtown_district",
|
| 24 |
+
"median_age": 32,
|
| 25 |
+
"median_income": 75000,
|
| 26 |
+
"poverty_rate": 0.15,
|
| 27 |
+
"racial_diversity_index": 0.72,
|
| 28 |
+
"language_diversity": 8,
|
| 29 |
+
"homeownership_rate": 0.25,
|
| 30 |
+
"resident_stability": 0.35,
|
| 31 |
+
"community_organization_strength": 6,
|
| 32 |
+
"gentrification_pressure": 8,
|
| 33 |
+
"recent_demographic_changes": [
|
| 34 |
+
"Influx of young professionals",
|
| 35 |
+
"Displacement of long-time low-income residents",
|
| 36 |
+
"Growing immigrant communities"
|
| 37 |
+
],
|
| 38 |
+
"cultural_character": "Dynamic and diverse urban core transitioning from working-class to professional. Mix of long-time residents, new arrivals, artists, and tech workers. Active nightlife and cultural scene. Tensions around affordability and displacement."
|
| 39 |
+
},
|
| 40 |
+
"temporal_context": {
|
| 41 |
+
"time_of_day": "evening",
|
| 42 |
+
"day_of_week": "Wednesday",
|
| 43 |
+
"season": "spring",
|
| 44 |
+
"recent_events": [
|
| 45 |
+
"Community meeting on proposed bike lane last week",
|
| 46 |
+
"New luxury apartment building opened last month",
|
| 47 |
+
"Small business forum on rising rents"
|
| 48 |
+
],
|
| 49 |
+
"upcoming_decisions": [
|
| 50 |
+
"City Council vote on bike lane next month",
|
| 51 |
+
"Zoning change proposal for Main Street",
|
| 52 |
+
"Affordable housing bond measure"
|
| 53 |
+
],
|
| 54 |
+
"weather": "mild, clear evening",
|
| 55 |
+
"special_circumstances": [
|
| 56 |
+
"Main Street reconstruction starting next month",
|
| 57 |
+
"Local arts festival this weekend"
|
| 58 |
+
]
|
| 59 |
+
},
|
| 60 |
+
"economic_context": {
|
| 61 |
+
"location_id": "downtown_district",
|
| 62 |
+
"unemployment_rate": 0.045,
|
| 63 |
+
"major_employers": [
|
| 64 |
+
"Tech startups",
|
| 65 |
+
"City government",
|
| 66 |
+
"Hospital",
|
| 67 |
+
"University"
|
| 68 |
+
],
|
| 69 |
+
"job_growth_rate": 0.035,
|
| 70 |
+
"small_business_density": 9,
|
| 71 |
+
"commercial_vacancy_rate": 0.08,
|
| 72 |
+
"median_home_price": 650000,
|
| 73 |
+
"median_rent": 2200,
|
| 74 |
+
"housing_cost_burden": 0.45,
|
| 75 |
+
"recent_investment": [
|
| 76 |
+
"New transit station ($50M)",
|
| 77 |
+
"Three new apartment buildings (500 units total)",
|
| 78 |
+
"Main Street streetscape improvements ($5M)"
|
| 79 |
+
],
|
| 80 |
+
"planned_developments": [
|
| 81 |
+
"Mixed-use development at old post office site",
|
| 82 |
+
"Waterfront park expansion",
|
| 83 |
+
"Affordable housing complex (100 units)"
|
| 84 |
+
],
|
| 85 |
+
"economic_trends": "Rapid growth and investment driving rising property values and rents. Small businesses struggling with increased costs. Strong job market but housing affordability crisis. Major public investment in transit and infrastructure attracting more development."
|
| 86 |
+
},
|
| 87 |
+
"metadata": {
|
| 88 |
+
"last_updated": "2024-03-15",
|
| 89 |
+
"data_sources": [
|
| 90 |
+
"US Census",
|
| 91 |
+
"City Planning Department",
|
| 92 |
+
"Local surveys"
|
| 93 |
+
]
|
| 94 |
+
}
|
| 95 |
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"persona_id": "david_kim",
|
| 3 |
+
"name": "David Kim",
|
| 4 |
+
"role": "Real Estate Developer",
|
| 5 |
+
"tagline": "Market-driven developer focused on urban growth and investment opportunities",
|
| 6 |
+
"demographics": {
|
| 7 |
+
"age": 46,
|
| 8 |
+
"gender": "male",
|
| 9 |
+
"education": "masters",
|
| 10 |
+
"occupation": "Real Estate Developer",
|
| 11 |
+
"income_level": "very_high",
|
| 12 |
+
"location_type": "suburban",
|
| 13 |
+
"years_in_community": 15,
|
| 14 |
+
"household_size": 4,
|
| 15 |
+
"has_children": true,
|
| 16 |
+
"owns_home": true,
|
| 17 |
+
"commute_method": "car"
|
| 18 |
+
},
|
| 19 |
+
"psychographics": {
|
| 20 |
+
"core_values": [
|
| 21 |
+
"market efficiency",
|
| 22 |
+
"economic growth",
|
| 23 |
+
"innovation",
|
| 24 |
+
"property rights"
|
| 25 |
+
],
|
| 26 |
+
"priorities": [
|
| 27 |
+
"return on investment",
|
| 28 |
+
"reducing regulations",
|
| 29 |
+
"market responsiveness",
|
| 30 |
+
"density increases",
|
| 31 |
+
"transit-oriented development"
|
| 32 |
+
],
|
| 33 |
+
"political_leaning": "conservative",
|
| 34 |
+
"openness_to_change": 8,
|
| 35 |
+
"community_engagement": 3,
|
| 36 |
+
"environmental_concern": 6,
|
| 37 |
+
"economic_focus": 10,
|
| 38 |
+
"social_equity_focus": 3,
|
| 39 |
+
"risk_tolerance": 9
|
| 40 |
+
},
|
| 41 |
+
"behavioral_profile": {
|
| 42 |
+
"communication_style": "polished and business-oriented, emphasizes economic benefits",
|
| 43 |
+
"decision_making_approach": "market-driven, financial analysis focused",
|
| 44 |
+
"conflict_resolution_style": "transactional, seeks negotiated deals",
|
| 45 |
+
"typical_concerns": [
|
| 46 |
+
"regulatory barriers",
|
| 47 |
+
"project feasibility",
|
| 48 |
+
"approval timelines",
|
| 49 |
+
"construction costs",
|
| 50 |
+
"market demand",
|
| 51 |
+
"zoning restrictions"
|
| 52 |
+
],
|
| 53 |
+
"typical_language_patterns": [
|
| 54 |
+
"The market is telling us...",
|
| 55 |
+
"This will create jobs",
|
| 56 |
+
"We need to reduce red tape",
|
| 57 |
+
"The numbers show...",
|
| 58 |
+
"This is good for tax revenue",
|
| 59 |
+
"Housing supply and demand",
|
| 60 |
+
"Public-private partnership"
|
| 61 |
+
],
|
| 62 |
+
"engagement_preferences": [
|
| 63 |
+
"one-on-one meetings with officials",
|
| 64 |
+
"business roundtables",
|
| 65 |
+
"formal presentations",
|
| 66 |
+
"lobbying"
|
| 67 |
+
]
|
| 68 |
+
},
|
| 69 |
+
"knowledge_domains": [
|
| 70 |
+
{
|
| 71 |
+
"domain": "Real Estate Development",
|
| 72 |
+
"expertise_level": 10,
|
| 73 |
+
"experience_years": 20
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"domain": "Finance and Investment",
|
| 77 |
+
"expertise_level": 9,
|
| 78 |
+
"experience_years": 22
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"domain": "Urban Economics",
|
| 82 |
+
"expertise_level": 8,
|
| 83 |
+
"experience_years": 20
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"domain": "Real Estate Law",
|
| 87 |
+
"expertise_level": 7,
|
| 88 |
+
"experience_years": 20
|
| 89 |
+
}
|
| 90 |
+
],
|
| 91 |
+
"background_story": "David earned his MBA from Wharton and started in commercial real estate before founding his own development firm 15 years ago. His company has built several mixed-use projects in the city and surrounding areas. He lives in an upscale suburban neighborhood with his wife and two children. David sees himself as a job creator and believes market forces should drive development with minimal regulation. He's become more interested in sustainable building practices when they align with market demand and can command premium prices. He's often at odds with community activists but has learned to navigate the political process effectively.",
|
| 92 |
+
"affiliated_organizations": [
|
| 93 |
+
"Urban Land Institute",
|
| 94 |
+
"Real Estate Development Association",
|
| 95 |
+
"Chamber of Commerce",
|
| 96 |
+
"Downtown Business Alliance"
|
| 97 |
+
]
|
| 98 |
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"persona_id": "elena_rodriguez",
|
| 3 |
+
"name": "Dr. Elena Rodriguez",
|
| 4 |
+
"role": "Transportation Engineer",
|
| 5 |
+
"tagline": "Data-driven transportation engineer focused on efficiency and safety",
|
| 6 |
+
"demographics": {
|
| 7 |
+
"age": 43,
|
| 8 |
+
"gender": "female",
|
| 9 |
+
"education": "doctorate",
|
| 10 |
+
"occupation": "Transportation Engineer",
|
| 11 |
+
"income_level": "high",
|
| 12 |
+
"location_type": "suburban",
|
| 13 |
+
"years_in_community": 12,
|
| 14 |
+
"household_size": 3,
|
| 15 |
+
"has_children": true,
|
| 16 |
+
"owns_home": true,
|
| 17 |
+
"commute_method": "car"
|
| 18 |
+
},
|
| 19 |
+
"psychographics": {
|
| 20 |
+
"core_values": [
|
| 21 |
+
"safety",
|
| 22 |
+
"efficiency",
|
| 23 |
+
"evidence-based practice",
|
| 24 |
+
"technical excellence"
|
| 25 |
+
],
|
| 26 |
+
"priorities": [
|
| 27 |
+
"traffic safety",
|
| 28 |
+
"system optimization",
|
| 29 |
+
"multimodal integration",
|
| 30 |
+
"data-driven decisions",
|
| 31 |
+
"maintenance costs"
|
| 32 |
+
],
|
| 33 |
+
"political_leaning": "moderate",
|
| 34 |
+
"openness_to_change": 7,
|
| 35 |
+
"community_engagement": 4,
|
| 36 |
+
"environmental_concern": 6,
|
| 37 |
+
"economic_focus": 7,
|
| 38 |
+
"social_equity_focus": 5,
|
| 39 |
+
"risk_tolerance": 5
|
| 40 |
+
},
|
| 41 |
+
"behavioral_profile": {
|
| 42 |
+
"communication_style": "technical and precise, emphasizes metrics and standards",
|
| 43 |
+
"decision_making_approach": "rigorous data analysis and engineering standards",
|
| 44 |
+
"conflict_resolution_style": "fact-based, focuses on objective criteria",
|
| 45 |
+
"typical_concerns": [
|
| 46 |
+
"safety metrics",
|
| 47 |
+
"traffic flow capacity",
|
| 48 |
+
"design standards compliance",
|
| 49 |
+
"cost-benefit ratios",
|
| 50 |
+
"maintenance requirements",
|
| 51 |
+
"liability issues"
|
| 52 |
+
],
|
| 53 |
+
"typical_language_patterns": [
|
| 54 |
+
"According to the data...",
|
| 55 |
+
"The engineering standards specify...",
|
| 56 |
+
"We need to model this",
|
| 57 |
+
"What's the level of service?",
|
| 58 |
+
"Safety is paramount",
|
| 59 |
+
"The metrics show..."
|
| 60 |
+
],
|
| 61 |
+
"engagement_preferences": [
|
| 62 |
+
"technical presentations",
|
| 63 |
+
"formal reports",
|
| 64 |
+
"professional conferences",
|
| 65 |
+
"email correspondence"
|
| 66 |
+
]
|
| 67 |
+
},
|
| 68 |
+
"knowledge_domains": [
|
| 69 |
+
{
|
| 70 |
+
"domain": "Transportation Engineering",
|
| 71 |
+
"expertise_level": 10,
|
| 72 |
+
"experience_years": 18
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"domain": "Traffic Safety",
|
| 76 |
+
"expertise_level": 9,
|
| 77 |
+
"experience_years": 18
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"domain": "Data Analysis",
|
| 81 |
+
"expertise_level": 8,
|
| 82 |
+
"experience_years": 20
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
"domain": "Urban Mobility",
|
| 86 |
+
"expertise_level": 8,
|
| 87 |
+
"experience_years": 15
|
| 88 |
+
}
|
| 89 |
+
],
|
| 90 |
+
"background_story": "Dr. Rodriguez earned her PhD in Civil Engineering from UC Berkeley with a focus on transportation systems. She has worked on major infrastructure projects across the country and joined the city's transportation department 12 years ago as Chief Transportation Engineer. She lives in the suburbs with her husband and teenage daughter. Elena values rigorous analysis and professional standards, sometimes clashing with what she sees as 'trendy' planning ideas that lack solid engineering foundations. However, she's become more open to innovative approaches when supported by data.",
|
| 91 |
+
"affiliated_organizations": [
|
| 92 |
+
"City Transportation Department",
|
| 93 |
+
"Institute of Transportation Engineers",
|
| 94 |
+
"Transportation Research Board",
|
| 95 |
+
"Society of Women Engineers"
|
| 96 |
+
]
|
| 97 |
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"persona_id": "james_obrien",
|
| 3 |
+
"name": "James O'Brien",
|
| 4 |
+
"role": "Long-time Resident",
|
| 5 |
+
"tagline": "Retired teacher and lifelong resident concerned about preserving neighborhood character",
|
| 6 |
+
"demographics": {
|
| 7 |
+
"age": 68,
|
| 8 |
+
"gender": "male",
|
| 9 |
+
"education": "bachelors",
|
| 10 |
+
"occupation": "Retired Teacher",
|
| 11 |
+
"income_level": "lower_middle",
|
| 12 |
+
"location_type": "urban",
|
| 13 |
+
"years_in_community": 68,
|
| 14 |
+
"household_size": 2,
|
| 15 |
+
"has_children": true,
|
| 16 |
+
"owns_home": true,
|
| 17 |
+
"commute_method": "walking"
|
| 18 |
+
},
|
| 19 |
+
"psychographics": {
|
| 20 |
+
"core_values": [
|
| 21 |
+
"tradition",
|
| 22 |
+
"community stability",
|
| 23 |
+
"respect for history",
|
| 24 |
+
"neighborliness"
|
| 25 |
+
],
|
| 26 |
+
"priorities": [
|
| 27 |
+
"preserving neighborhood character",
|
| 28 |
+
"preventing displacement",
|
| 29 |
+
"maintaining property values",
|
| 30 |
+
"reducing traffic",
|
| 31 |
+
"public safety"
|
| 32 |
+
],
|
| 33 |
+
"political_leaning": "conservative",
|
| 34 |
+
"openness_to_change": 3,
|
| 35 |
+
"community_engagement": 9,
|
| 36 |
+
"environmental_concern": 5,
|
| 37 |
+
"economic_focus": 6,
|
| 38 |
+
"social_equity_focus": 4,
|
| 39 |
+
"risk_tolerance": 2
|
| 40 |
+
},
|
| 41 |
+
"behavioral_profile": {
|
| 42 |
+
"communication_style": "storytelling, draws on personal history and anecdotes",
|
| 43 |
+
"decision_making_approach": "traditional, based on past experience",
|
| 44 |
+
"conflict_resolution_style": "can be oppositional but responds to personal relationships",
|
| 45 |
+
"typical_concerns": [
|
| 46 |
+
"changes to neighborhood character",
|
| 47 |
+
"increased traffic and congestion",
|
| 48 |
+
"new residents changing culture",
|
| 49 |
+
"property tax increases",
|
| 50 |
+
"loss of familiar landmarks",
|
| 51 |
+
"personal safety"
|
| 52 |
+
],
|
| 53 |
+
"typical_language_patterns": [
|
| 54 |
+
"I've lived here my whole life...",
|
| 55 |
+
"Back in the day...",
|
| 56 |
+
"This used to be a quiet neighborhood",
|
| 57 |
+
"I remember when...",
|
| 58 |
+
"What about us long-time residents?",
|
| 59 |
+
"You're going to ruin this neighborhood"
|
| 60 |
+
],
|
| 61 |
+
"engagement_preferences": [
|
| 62 |
+
"neighborhood association meetings",
|
| 63 |
+
"public hearings",
|
| 64 |
+
"door-to-door conversations",
|
| 65 |
+
"community forums"
|
| 66 |
+
]
|
| 67 |
+
},
|
| 68 |
+
"knowledge_domains": [
|
| 69 |
+
{
|
| 70 |
+
"domain": "Local History",
|
| 71 |
+
"expertise_level": 10,
|
| 72 |
+
"experience_years": 68
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"domain": "Community Organizing",
|
| 76 |
+
"expertise_level": 7,
|
| 77 |
+
"experience_years": 30
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"domain": "Education",
|
| 81 |
+
"expertise_level": 8,
|
| 82 |
+
"experience_years": 35
|
| 83 |
+
}
|
| 84 |
+
],
|
| 85 |
+
"background_story": "James was born in this neighborhood 68 years ago and has never lived anywhere else. He taught at the local elementary school for 35 years before retiring. He bought his modest home 45 years ago and has watched the neighborhood evolve through multiple waves of change. James is active in the neighborhood association and known for attending every community meeting. While seen as resistant to change, he's deeply knowledgeable about local history and genuinely concerned about long-time residents being priced out. He walks everywhere in the neighborhood and knows everyone by name.",
|
| 86 |
+
"affiliated_organizations": [
|
| 87 |
+
"Neighborhood Association",
|
| 88 |
+
"Historical Society",
|
| 89 |
+
"Retired Teachers Association",
|
| 90 |
+
"Local Church Council"
|
| 91 |
+
]
|
| 92 |
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"persona_id": "marcus_thompson",
|
| 3 |
+
"name": "Marcus Thompson",
|
| 4 |
+
"role": "Local Business Owner",
|
| 5 |
+
"tagline": "Pragmatic small business owner focused on economic vitality and accessibility",
|
| 6 |
+
"demographics": {
|
| 7 |
+
"age": 52,
|
| 8 |
+
"gender": "male",
|
| 9 |
+
"education": "bachelors",
|
| 10 |
+
"occupation": "Restaurant Owner",
|
| 11 |
+
"income_level": "middle",
|
| 12 |
+
"location_type": "urban",
|
| 13 |
+
"years_in_community": 28,
|
| 14 |
+
"household_size": 4,
|
| 15 |
+
"has_children": true,
|
| 16 |
+
"owns_home": true,
|
| 17 |
+
"commute_method": "car"
|
| 18 |
+
},
|
| 19 |
+
"psychographics": {
|
| 20 |
+
"core_values": [
|
| 21 |
+
"economic opportunity",
|
| 22 |
+
"community stability",
|
| 23 |
+
"practical solutions",
|
| 24 |
+
"local character"
|
| 25 |
+
],
|
| 26 |
+
"priorities": [
|
| 27 |
+
"business accessibility",
|
| 28 |
+
"parking availability",
|
| 29 |
+
"foot traffic",
|
| 30 |
+
"property values",
|
| 31 |
+
"crime prevention"
|
| 32 |
+
],
|
| 33 |
+
"political_leaning": "moderate",
|
| 34 |
+
"openness_to_change": 5,
|
| 35 |
+
"community_engagement": 7,
|
| 36 |
+
"environmental_concern": 5,
|
| 37 |
+
"economic_focus": 9,
|
| 38 |
+
"social_equity_focus": 6,
|
| 39 |
+
"risk_tolerance": 4
|
| 40 |
+
},
|
| 41 |
+
"behavioral_profile": {
|
| 42 |
+
"communication_style": "direct and practical, focuses on real-world impacts",
|
| 43 |
+
"decision_making_approach": "pragmatic, bottom-line oriented",
|
| 44 |
+
"conflict_resolution_style": "assertive but willing to compromise",
|
| 45 |
+
"typical_concerns": [
|
| 46 |
+
"impact on customer access",
|
| 47 |
+
"construction disruption",
|
| 48 |
+
"parking reduction",
|
| 49 |
+
"cost to businesses",
|
| 50 |
+
"implementation timeline"
|
| 51 |
+
],
|
| 52 |
+
"typical_language_patterns": [
|
| 53 |
+
"How will this affect my customers?",
|
| 54 |
+
"We need to think about the businesses",
|
| 55 |
+
"That sounds expensive",
|
| 56 |
+
"Will there be parking?",
|
| 57 |
+
"I've been here 28 years and...",
|
| 58 |
+
"Small businesses can't afford..."
|
| 59 |
+
],
|
| 60 |
+
"engagement_preferences": [
|
| 61 |
+
"business association meetings",
|
| 62 |
+
"chamber of commerce events",
|
| 63 |
+
"one-on-one meetings",
|
| 64 |
+
"site visits"
|
| 65 |
+
]
|
| 66 |
+
},
|
| 67 |
+
"knowledge_domains": [
|
| 68 |
+
{
|
| 69 |
+
"domain": "Small Business Management",
|
| 70 |
+
"expertise_level": 9,
|
| 71 |
+
"experience_years": 28
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
"domain": "Local Economic Development",
|
| 75 |
+
"expertise_level": 7,
|
| 76 |
+
"experience_years": 28
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"domain": "Restaurant Industry",
|
| 80 |
+
"expertise_level": 9,
|
| 81 |
+
"experience_years": 28
|
| 82 |
+
}
|
| 83 |
+
],
|
| 84 |
+
"background_story": "Marcus opened his soul food restaurant on Main Street 28 years ago and has watched the neighborhood change significantly. He's raised three kids in the community and owns a home in the nearby residential area. As president of the Main Street Business Association, he advocates for local merchants. While he supports some improvements, he's cautious about changes that might disrupt his business or alienate existing customers. His restaurant is a community gathering place.",
|
| 85 |
+
"affiliated_organizations": [
|
| 86 |
+
"Main Street Business Association",
|
| 87 |
+
"Chamber of Commerce",
|
| 88 |
+
"Local Restaurant Coalition"
|
| 89 |
+
]
|
| 90 |
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"persona_id": "priya_patel",
|
| 3 |
+
"name": "Priya Patel",
|
| 4 |
+
"role": "Housing Advocate",
|
| 5 |
+
"tagline": "Young activist organizing for housing justice and equitable development",
|
| 6 |
+
"demographics": {
|
| 7 |
+
"age": 28,
|
| 8 |
+
"gender": "female",
|
| 9 |
+
"education": "bachelors",
|
| 10 |
+
"occupation": "Nonprofit Organizer",
|
| 11 |
+
"income_level": "low",
|
| 12 |
+
"location_type": "urban",
|
| 13 |
+
"years_in_community": 6,
|
| 14 |
+
"household_size": 3,
|
| 15 |
+
"has_children": false,
|
| 16 |
+
"owns_home": false,
|
| 17 |
+
"commute_method": "bus"
|
| 18 |
+
},
|
| 19 |
+
"psychographics": {
|
| 20 |
+
"core_values": [
|
| 21 |
+
"social justice",
|
| 22 |
+
"equity",
|
| 23 |
+
"community empowerment",
|
| 24 |
+
"anti-displacement"
|
| 25 |
+
],
|
| 26 |
+
"priorities": [
|
| 27 |
+
"affordable housing",
|
| 28 |
+
"tenant protections",
|
| 29 |
+
"community benefits",
|
| 30 |
+
"racial equity",
|
| 31 |
+
"inclusive processes"
|
| 32 |
+
],
|
| 33 |
+
"political_leaning": "very_progressive",
|
| 34 |
+
"openness_to_change": 8,
|
| 35 |
+
"community_engagement": 10,
|
| 36 |
+
"environmental_concern": 8,
|
| 37 |
+
"economic_focus": 4,
|
| 38 |
+
"social_equity_focus": 10,
|
| 39 |
+
"risk_tolerance": 8
|
| 40 |
+
},
|
| 41 |
+
"behavioral_profile": {
|
| 42 |
+
"communication_style": "passionate and rights-focused, uses social justice framing",
|
| 43 |
+
"decision_making_approach": "community-centered, prioritizes most vulnerable",
|
| 44 |
+
"conflict_resolution_style": "confrontational when needed, but seeks transformative solutions",
|
| 45 |
+
"typical_concerns": [
|
| 46 |
+
"displacement of low-income residents",
|
| 47 |
+
"gentrification",
|
| 48 |
+
"community benefits",
|
| 49 |
+
"representation in decision-making",
|
| 50 |
+
"racial and economic justice",
|
| 51 |
+
"corporate influence"
|
| 52 |
+
],
|
| 53 |
+
"typical_language_patterns": [
|
| 54 |
+
"Who benefits from this?",
|
| 55 |
+
"This is a justice issue",
|
| 56 |
+
"We need community control",
|
| 57 |
+
"What about displacement?",
|
| 58 |
+
"Housing is a human right",
|
| 59 |
+
"This is environmental racism",
|
| 60 |
+
"Nothing about us without us"
|
| 61 |
+
],
|
| 62 |
+
"engagement_preferences": [
|
| 63 |
+
"community organizing",
|
| 64 |
+
"protests and demonstrations",
|
| 65 |
+
"social media campaigns",
|
| 66 |
+
"grassroots meetings",
|
| 67 |
+
"coalition building"
|
| 68 |
+
]
|
| 69 |
+
},
|
| 70 |
+
"knowledge_domains": [
|
| 71 |
+
{
|
| 72 |
+
"domain": "Housing Policy",
|
| 73 |
+
"expertise_level": 8,
|
| 74 |
+
"experience_years": 6
|
| 75 |
+
},
|
| 76 |
+
{
|
| 77 |
+
"domain": "Community Organizing",
|
| 78 |
+
"expertise_level": 9,
|
| 79 |
+
"experience_years": 8
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"domain": "Social Justice",
|
| 83 |
+
"expertise_level": 8,
|
| 84 |
+
"experience_years": 10
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"domain": "Tenant Rights",
|
| 88 |
+
"expertise_level": 7,
|
| 89 |
+
"experience_years": 6
|
| 90 |
+
}
|
| 91 |
+
],
|
| 92 |
+
"background_story": "Priya grew up in a working-class immigrant family and was politicized by her family's housing struggles. She studied sociology and ethnic studies in college and moved to the city 6 years ago to work for a housing justice nonprofit. She lives with roommates in a rent-controlled apartment and relies on public transit. Priya is a skilled organizer who builds coalitions across communities of color and low-income residents. While some see her as radical, she's effective at mobilizing community voice and has successfully fought several displacement-causing developments. She's frustrated by what she sees as tokenistic community engagement in planning processes.",
|
| 93 |
+
"affiliated_organizations": [
|
| 94 |
+
"Housing Justice Coalition",
|
| 95 |
+
"Tenants Union",
|
| 96 |
+
"Community Land Trust Board",
|
| 97 |
+
"Racial Justice Alliance"
|
| 98 |
+
]
|
| 99 |
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"persona_id": "sarah_chen",
|
| 3 |
+
"name": "Sarah Chen",
|
| 4 |
+
"role": "Urban Planner",
|
| 5 |
+
"tagline": "Progressive city planner focused on sustainable, walkable communities",
|
| 6 |
+
"demographics": {
|
| 7 |
+
"age": 34,
|
| 8 |
+
"gender": "female",
|
| 9 |
+
"education": "masters",
|
| 10 |
+
"occupation": "Urban Planner",
|
| 11 |
+
"income_level": "upper_middle",
|
| 12 |
+
"location_type": "urban",
|
| 13 |
+
"years_in_community": 8,
|
| 14 |
+
"household_size": 2,
|
| 15 |
+
"has_children": false,
|
| 16 |
+
"owns_home": false,
|
| 17 |
+
"commute_method": "bike"
|
| 18 |
+
},
|
| 19 |
+
"psychographics": {
|
| 20 |
+
"core_values": [
|
| 21 |
+
"sustainability",
|
| 22 |
+
"innovation",
|
| 23 |
+
"equity",
|
| 24 |
+
"community health"
|
| 25 |
+
],
|
| 26 |
+
"priorities": [
|
| 27 |
+
"climate action",
|
| 28 |
+
"active transportation",
|
| 29 |
+
"mixed-use development",
|
| 30 |
+
"affordable housing",
|
| 31 |
+
"public spaces"
|
| 32 |
+
],
|
| 33 |
+
"political_leaning": "progressive",
|
| 34 |
+
"openness_to_change": 9,
|
| 35 |
+
"community_engagement": 8,
|
| 36 |
+
"environmental_concern": 10,
|
| 37 |
+
"economic_focus": 6,
|
| 38 |
+
"social_equity_focus": 9,
|
| 39 |
+
"risk_tolerance": 7
|
| 40 |
+
},
|
| 41 |
+
"behavioral_profile": {
|
| 42 |
+
"communication_style": "analytical and passionate, uses data and research to support vision",
|
| 43 |
+
"decision_making_approach": "evidence-based with strong values framework",
|
| 44 |
+
"conflict_resolution_style": "collaborative, seeks win-win solutions",
|
| 45 |
+
"typical_concerns": [
|
| 46 |
+
"environmental impact",
|
| 47 |
+
"accessibility for all abilities",
|
| 48 |
+
"long-term sustainability",
|
| 49 |
+
"community character",
|
| 50 |
+
"displacement of existing residents"
|
| 51 |
+
],
|
| 52 |
+
"typical_language_patterns": [
|
| 53 |
+
"Let's look at the data...",
|
| 54 |
+
"What does best practice tell us?",
|
| 55 |
+
"We need to think about future generations",
|
| 56 |
+
"How does this support our climate goals?",
|
| 57 |
+
"I'm concerned about equity implications"
|
| 58 |
+
],
|
| 59 |
+
"engagement_preferences": [
|
| 60 |
+
"community workshops",
|
| 61 |
+
"design charrettes",
|
| 62 |
+
"online platforms",
|
| 63 |
+
"walking tours"
|
| 64 |
+
]
|
| 65 |
+
},
|
| 66 |
+
"knowledge_domains": [
|
| 67 |
+
{
|
| 68 |
+
"domain": "Urban Planning",
|
| 69 |
+
"expertise_level": 9,
|
| 70 |
+
"experience_years": 10
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"domain": "Sustainability",
|
| 74 |
+
"expertise_level": 8,
|
| 75 |
+
"experience_years": 10
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"domain": "Transportation Planning",
|
| 79 |
+
"expertise_level": 7,
|
| 80 |
+
"experience_years": 8
|
| 81 |
+
}
|
| 82 |
+
],
|
| 83 |
+
"background_story": "Sarah earned her Master's in Urban Planning from MIT, focusing on climate-responsive design. She moved to the city 8 years ago for her job at the planning department. She lives in a downtown apartment with her partner and bikes to work daily. Sarah is passionate about creating vibrant, sustainable neighborhoods and frequently attends community meetings. She believes strongly in data-driven planning but also values community input and indigenous knowledge.",
|
| 84 |
+
"affiliated_organizations": [
|
| 85 |
+
"City Planning Department",
|
| 86 |
+
"American Planning Association",
|
| 87 |
+
"Local Bike Coalition",
|
| 88 |
+
"Climate Action Task Force"
|
| 89 |
+
]
|
| 90 |
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Getting Started with AI Personas
|
| 2 |
+
|
| 3 |
+
This guide will help you set up and start using the AI Personas system for urban planning.
|
| 4 |
+
|
| 5 |
+
## Prerequisites
|
| 6 |
+
|
| 7 |
+
- Python 3.11 or higher
|
| 8 |
+
- Anthropic API key (get one at https://console.anthropic.com/)
|
| 9 |
+
- Basic understanding of urban planning concepts (helpful but not required)
|
| 10 |
+
|
| 11 |
+
## Installation
|
| 12 |
+
|
| 13 |
+
### 1. Clone and Setup
|
| 14 |
+
|
| 15 |
+
```bash
|
| 16 |
+
cd AI_Personas
|
| 17 |
+
|
| 18 |
+
# Create virtual environment
|
| 19 |
+
python -m venv venv
|
| 20 |
+
|
| 21 |
+
# Activate virtual environment
|
| 22 |
+
# On macOS/Linux:
|
| 23 |
+
source venv/bin/activate
|
| 24 |
+
# On Windows:
|
| 25 |
+
# venv\Scripts\activate
|
| 26 |
+
|
| 27 |
+
# Install dependencies
|
| 28 |
+
pip install -r requirements.txt
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
### 2. Configure API Key
|
| 32 |
+
|
| 33 |
+
```bash
|
| 34 |
+
# Copy the environment template
|
| 35 |
+
cp .env.example .env
|
| 36 |
+
|
| 37 |
+
# Edit .env and add your Anthropic API key
|
| 38 |
+
# ANTHROPIC_API_KEY=your_actual_key_here
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
**Important:** Never commit your `.env` file to version control!
|
| 42 |
+
|
| 43 |
+
## Quick Start
|
| 44 |
+
|
| 45 |
+
### Test the System
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
# Run a simple test query
|
| 49 |
+
python examples/phase1_simple_query.py
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
This will query Sarah Chen (our urban planner persona) about a bike lane proposal.
|
| 53 |
+
|
| 54 |
+
### Interactive CLI
|
| 55 |
+
|
| 56 |
+
```bash
|
| 57 |
+
# Launch the interactive command-line interface
|
| 58 |
+
python -m src.cli
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
The CLI allows you to:
|
| 62 |
+
- Query individual personas interactively
|
| 63 |
+
- Query all personas with the same question
|
| 64 |
+
- Browse available personas and contexts
|
| 65 |
+
- Experiment with different scenarios
|
| 66 |
+
|
| 67 |
+
### Query Multiple Perspectives
|
| 68 |
+
|
| 69 |
+
```bash
|
| 70 |
+
# See how all 6 stakeholders respond to the same issue
|
| 71 |
+
python examples/phase1_multiple_perspectives.py
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
This demonstrates the power of the system: seeing diverse stakeholder perspectives on the same urban planning issue.
|
| 75 |
+
|
| 76 |
+
## Understanding the System
|
| 77 |
+
|
| 78 |
+
### The 6 Personas
|
| 79 |
+
|
| 80 |
+
The system includes 6 diverse urban planning stakeholders:
|
| 81 |
+
|
| 82 |
+
1. **Sarah Chen** - Progressive urban planner focused on sustainability
|
| 83 |
+
2. **Marcus Thompson** - Local business owner concerned about economic impacts
|
| 84 |
+
3. **Dr. Elena Rodriguez** - Data-driven transportation engineer
|
| 85 |
+
4. **James O'Brien** - Long-time resident protective of neighborhood character
|
| 86 |
+
5. **Priya Patel** - Young housing justice advocate
|
| 87 |
+
6. **David Kim** - Market-driven real estate developer
|
| 88 |
+
|
| 89 |
+
Each persona has:
|
| 90 |
+
- Detailed demographics and background
|
| 91 |
+
- Core values and priorities
|
| 92 |
+
- Communication style and language patterns
|
| 93 |
+
- Domain expertise
|
| 94 |
+
- Typical concerns
|
| 95 |
+
|
| 96 |
+
### Environmental Contexts
|
| 97 |
+
|
| 98 |
+
Contexts provide situational information that influences responses:
|
| 99 |
+
|
| 100 |
+
- **Built environment**: Density, transit access, amenities
|
| 101 |
+
- **Social context**: Demographics, community characteristics
|
| 102 |
+
- **Economic context**: Housing costs, employment, development
|
| 103 |
+
- **Temporal context**: Time, season, recent events
|
| 104 |
+
|
| 105 |
+
The system currently includes one sample context: `downtown_district`
|
| 106 |
+
|
| 107 |
+
## Basic Usage Examples
|
| 108 |
+
|
| 109 |
+
### Python API
|
| 110 |
+
|
| 111 |
+
```python
|
| 112 |
+
from src.pipeline.query_engine import QueryEngine
|
| 113 |
+
|
| 114 |
+
# Initialize
|
| 115 |
+
engine = QueryEngine()
|
| 116 |
+
|
| 117 |
+
# Query a single persona
|
| 118 |
+
response = engine.query(
|
| 119 |
+
persona_id="sarah_chen",
|
| 120 |
+
question="What do you think about adding bike lanes?",
|
| 121 |
+
context_id="downtown_district"
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
print(f"{response.persona_name}: {response.response}")
|
| 125 |
+
|
| 126 |
+
# Query multiple personas
|
| 127 |
+
responses = engine.query_multiple(
|
| 128 |
+
persona_ids=["sarah_chen", "marcus_thompson", "elena_rodriguez"],
|
| 129 |
+
question="Should we reduce parking for bike lanes?",
|
| 130 |
+
context_id="downtown_district"
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
for r in responses:
|
| 134 |
+
print(f"\n{r.persona_name} ({r.persona_role}):")
|
| 135 |
+
print(r.response)
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
### Custom Scenarios
|
| 139 |
+
|
| 140 |
+
You can add scenario descriptions for more specific queries:
|
| 141 |
+
|
| 142 |
+
```python
|
| 143 |
+
scenario = """
|
| 144 |
+
The city is considering a $50M bond measure to fund:
|
| 145 |
+
- 20 miles of protected bike lanes
|
| 146 |
+
- Three new bus rapid transit lines
|
| 147 |
+
- Pedestrian improvements in 10 neighborhoods
|
| 148 |
+
This would increase property taxes by $200/year for median homeowner.
|
| 149 |
+
"""
|
| 150 |
+
|
| 151 |
+
response = engine.query(
|
| 152 |
+
persona_id="james_obrien",
|
| 153 |
+
question="Would you support this bond measure?",
|
| 154 |
+
context_id="downtown_district",
|
| 155 |
+
scenario_description=scenario
|
| 156 |
+
)
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
## Creating Custom Personas
|
| 160 |
+
|
| 161 |
+
To add your own personas:
|
| 162 |
+
|
| 163 |
+
1. Copy an existing persona JSON file from `data/personas/`
|
| 164 |
+
2. Modify the attributes to match your new persona
|
| 165 |
+
3. Save with a new filename in the same directory
|
| 166 |
+
4. The system will automatically load it on next run
|
| 167 |
+
|
| 168 |
+
Key fields to customize:
|
| 169 |
+
- `persona_id`: Unique identifier (use snake_case)
|
| 170 |
+
- `name`, `role`, `tagline`: Basic identity
|
| 171 |
+
- `demographics`: Age, education, occupation, etc.
|
| 172 |
+
- `psychographics`: Values, priorities, political leaning
|
| 173 |
+
- `behavioral_profile`: Communication and decision-making style
|
| 174 |
+
- `background_story`: Narrative context
|
| 175 |
+
|
| 176 |
+
## Creating Custom Contexts
|
| 177 |
+
|
| 178 |
+
To add environmental contexts:
|
| 179 |
+
|
| 180 |
+
1. Copy `data/contexts/downtown_district.json`
|
| 181 |
+
2. Modify the fields to match your location
|
| 182 |
+
3. Save with a new filename in the same directory
|
| 183 |
+
|
| 184 |
+
## Tips for Effective Queries
|
| 185 |
+
|
| 186 |
+
1. **Be specific**: Instead of "What do you think about housing?", ask "Should we allow 4-story apartment buildings on Main Street?"
|
| 187 |
+
|
| 188 |
+
2. **Add context**: Use scenario descriptions to provide relevant details
|
| 189 |
+
|
| 190 |
+
3. **Compare perspectives**: Query multiple personas to see different viewpoints
|
| 191 |
+
|
| 192 |
+
4. **Use realistic situations**: Ground questions in actual planning decisions
|
| 193 |
+
|
| 194 |
+
5. **Consider timing**: Recent events in temporal context can influence responses
|
| 195 |
+
|
| 196 |
+
## Troubleshooting
|
| 197 |
+
|
| 198 |
+
### "No personas loaded"
|
| 199 |
+
- Check that JSON files exist in `data/personas/`
|
| 200 |
+
- Verify JSON syntax is valid
|
| 201 |
+
- Check file permissions
|
| 202 |
+
|
| 203 |
+
### "Anthropic API key must be provided"
|
| 204 |
+
- Ensure `.env` file exists
|
| 205 |
+
- Verify `ANTHROPIC_API_KEY` is set correctly
|
| 206 |
+
- Check that you've activated the virtual environment
|
| 207 |
+
|
| 208 |
+
### "Persona not found"
|
| 209 |
+
- Run `python -m src.cli` and select option 3 to list available personas
|
| 210 |
+
- Check spelling of persona_id (must match exactly)
|
| 211 |
+
|
| 212 |
+
### Poor quality responses
|
| 213 |
+
- Try adjusting temperature (lower = more consistent, higher = more creative)
|
| 214 |
+
- Provide more context via scenario descriptions
|
| 215 |
+
- Ensure persona definitions are detailed and realistic
|
| 216 |
+
|
| 217 |
+
## Next Steps
|
| 218 |
+
|
| 219 |
+
### Phase 2: Population Distributions (Coming Soon)
|
| 220 |
+
|
| 221 |
+
Phase 2 will enable:
|
| 222 |
+
- Generating 100s of persona variants with statistical distributions
|
| 223 |
+
- Analyzing response distributions (mean, variance, clusters)
|
| 224 |
+
- Visualizing how a population would respond
|
| 225 |
+
|
| 226 |
+
### Phase 3: Opinion Dynamics (Coming Soon)
|
| 227 |
+
|
| 228 |
+
Phase 3 will enable:
|
| 229 |
+
- Modeling influence between personas
|
| 230 |
+
- Running simulations to find opinion equilibria
|
| 231 |
+
- Discovering consensus and polarization patterns
|
| 232 |
+
|
| 233 |
+
## Support
|
| 234 |
+
|
| 235 |
+
- Check the main [README.md](../README.md) for overview
|
| 236 |
+
- Review example scripts in `examples/` directory
|
| 237 |
+
- Examine persona JSON files in `data/personas/` to understand structure
|
| 238 |
+
|
| 239 |
+
## Best Practices
|
| 240 |
+
|
| 241 |
+
1. **Version control personas**: Treat persona definitions as code
|
| 242 |
+
2. **Document assumptions**: Add notes in metadata fields
|
| 243 |
+
3. **Iterate on personas**: Refine based on response quality
|
| 244 |
+
4. **Mix perspectives**: Use diverse personas for balanced analysis
|
| 245 |
+
5. **Ground in reality**: Base personas on real stakeholder research
|
| 246 |
+
|
| 247 |
+
Happy planning! ποΈ
|
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Phase 1 Implementation Summary
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
Phase 1 of the AI Personas system is now complete! This phase provides a foundation for querying synthetic personas representing diverse urban planning stakeholders and receiving contextually-aware responses.
|
| 6 |
+
|
| 7 |
+
## What's Implemented
|
| 8 |
+
|
| 9 |
+
### Core Functionality
|
| 10 |
+
|
| 11 |
+
β
**Persona System**
|
| 12 |
+
- 6 diverse synthetic personas representing key urban stakeholders
|
| 13 |
+
- Comprehensive data models with demographics, psychographics, and behavioral profiles
|
| 14 |
+
- Persona database with search and filtering capabilities
|
| 15 |
+
- Easy-to-extend JSON-based persona definitions
|
| 16 |
+
|
| 17 |
+
β
**Environmental Context System**
|
| 18 |
+
- Multi-dimensional context modeling (built environment, social, economic, temporal)
|
| 19 |
+
- Sample downtown district context included
|
| 20 |
+
- Context database for managing multiple locations
|
| 21 |
+
- Extensible for adding new contexts
|
| 22 |
+
|
| 23 |
+
β
**LLM Integration**
|
| 24 |
+
- Anthropic Claude API integration
|
| 25 |
+
- Smart prompt construction from persona + context
|
| 26 |
+
- Support for conversation history
|
| 27 |
+
- Configurable temperature and token limits
|
| 28 |
+
|
| 29 |
+
β
**Query-Response Pipeline**
|
| 30 |
+
- End-to-end system for querying personas
|
| 31 |
+
- Single and multi-persona query support
|
| 32 |
+
- Structured response objects with metadata
|
| 33 |
+
- System health checking
|
| 34 |
+
|
| 35 |
+
β
**User Interfaces**
|
| 36 |
+
- Interactive CLI for exploration
|
| 37 |
+
- Example scripts demonstrating usage
|
| 38 |
+
- Python API for programmatic access
|
| 39 |
+
|
| 40 |
+
β
**Documentation**
|
| 41 |
+
- Comprehensive README
|
| 42 |
+
- Getting Started guide
|
| 43 |
+
- Example code
|
| 44 |
+
- Test suite
|
| 45 |
+
|
| 46 |
+
## The 6 Personas
|
| 47 |
+
|
| 48 |
+
1. **Sarah Chen** (34) - Urban Planner
|
| 49 |
+
- Progressive, sustainability-focused
|
| 50 |
+
- High environmental concern, data-driven
|
| 51 |
+
- Bikes to work, rents downtown
|
| 52 |
+
|
| 53 |
+
2. **Marcus Thompson** (52) - Restaurant Owner
|
| 54 |
+
- Moderate, economically pragmatic
|
| 55 |
+
- 28 years in community, Main Street Business Association president
|
| 56 |
+
- Concerned about parking and customer access
|
| 57 |
+
|
| 58 |
+
3. **Dr. Elena Rodriguez** (43) - Transportation Engineer
|
| 59 |
+
- Technical, evidence-based
|
| 60 |
+
- PhD from UC Berkeley, Chief Transportation Engineer
|
| 61 |
+
- Prioritizes safety metrics and engineering standards
|
| 62 |
+
|
| 63 |
+
4. **James O'Brien** (68) - Retired Teacher
|
| 64 |
+
- Conservative, tradition-oriented
|
| 65 |
+
- Lifelong resident, active in neighborhood association
|
| 66 |
+
- Resistant to change, concerned about neighborhood character
|
| 67 |
+
|
| 68 |
+
5. **Priya Patel** (28) - Housing Advocate
|
| 69 |
+
- Very progressive, justice-focused
|
| 70 |
+
- Nonprofit organizer, tenant rights activist
|
| 71 |
+
- Prioritizes equity and anti-displacement
|
| 72 |
+
|
| 73 |
+
6. **David Kim** (46) - Real Estate Developer
|
| 74 |
+
- Market-driven, growth-oriented
|
| 75 |
+
- MBA from Wharton, owns development firm
|
| 76 |
+
- Focuses on ROI and reducing regulations
|
| 77 |
+
|
| 78 |
+
## Key Features
|
| 79 |
+
|
| 80 |
+
### Authentic Responses
|
| 81 |
+
Each persona responds based on:
|
| 82 |
+
- Their values, priorities, and political orientation
|
| 83 |
+
- Professional expertise and life experience
|
| 84 |
+
- Communication style and typical concerns
|
| 85 |
+
- Current environmental context
|
| 86 |
+
|
| 87 |
+
### Contextual Awareness
|
| 88 |
+
Responses consider:
|
| 89 |
+
- Built environment characteristics
|
| 90 |
+
- Social and demographic context
|
| 91 |
+
- Economic conditions
|
| 92 |
+
- Recent events and upcoming decisions
|
| 93 |
+
|
| 94 |
+
### Multiple Perspectives
|
| 95 |
+
Easily query all personas with the same question to see:
|
| 96 |
+
- How different stakeholders frame issues
|
| 97 |
+
- What concerns each group prioritizes
|
| 98 |
+
- Where consensus or conflict exists
|
| 99 |
+
- How values shape interpretations
|
| 100 |
+
|
| 101 |
+
## Usage Examples
|
| 102 |
+
|
| 103 |
+
### Quick Start
|
| 104 |
+
```bash
|
| 105 |
+
# Test the system
|
| 106 |
+
python tests/test_basic_functionality.py
|
| 107 |
+
|
| 108 |
+
# Run a simple query
|
| 109 |
+
python examples/phase1_simple_query.py
|
| 110 |
+
|
| 111 |
+
# See multiple perspectives
|
| 112 |
+
python examples/phase1_multiple_perspectives.py
|
| 113 |
+
|
| 114 |
+
# Interactive exploration
|
| 115 |
+
python -m src.cli
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
### Python API
|
| 119 |
+
```python
|
| 120 |
+
from src.pipeline.query_engine import QueryEngine
|
| 121 |
+
|
| 122 |
+
engine = QueryEngine()
|
| 123 |
+
|
| 124 |
+
# Single persona query
|
| 125 |
+
response = engine.query(
|
| 126 |
+
persona_id="sarah_chen",
|
| 127 |
+
question="Should we add bike lanes on Main Street?",
|
| 128 |
+
context_id="downtown_district"
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
print(response.response)
|
| 132 |
+
|
| 133 |
+
# Multiple personas
|
| 134 |
+
responses = engine.query_multiple(
|
| 135 |
+
persona_ids=["sarah_chen", "marcus_thompson"],
|
| 136 |
+
question="What's your view on the parking reduction?",
|
| 137 |
+
context_id="downtown_district"
|
| 138 |
+
)
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
## Technical Architecture
|
| 142 |
+
|
| 143 |
+
```
|
| 144 |
+
AI_Personas/
|
| 145 |
+
βββ src/
|
| 146 |
+
β βββ personas/ # Persona data models and database
|
| 147 |
+
β βββ context/ # Environmental context system
|
| 148 |
+
β βββ llm/ # Anthropic Claude integration
|
| 149 |
+
β βββ pipeline/ # Query-response orchestration
|
| 150 |
+
β βββ cli.py # Interactive interface
|
| 151 |
+
βββ data/
|
| 152 |
+
β βββ personas/ # 6 persona JSON files
|
| 153 |
+
β βββ contexts/ # Environmental context data
|
| 154 |
+
βββ examples/ # Usage examples
|
| 155 |
+
βββ tests/ # Test suite
|
| 156 |
+
βββ docs/ # Documentation
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
## What's Next
|
| 160 |
+
|
| 161 |
+
### Phase 2: Population Response Distributions (Planned)
|
| 162 |
+
- Generate persona variants with statistical distributions
|
| 163 |
+
- Query populations of 100+ persona instances
|
| 164 |
+
- Analyze response distributions (mean, variance, clusters)
|
| 165 |
+
- Visualize opinion distributions
|
| 166 |
+
- Support different sampling methods (gaussian, uniform, bootstrap)
|
| 167 |
+
|
| 168 |
+
### Phase 3: Multi-Persona Influence & Equilibrium (Planned)
|
| 169 |
+
- Model social network graphs between personas
|
| 170 |
+
- Implement opinion dynamics models:
|
| 171 |
+
- DeGroot (weighted averaging)
|
| 172 |
+
- Bounded Confidence (Hegselmann-Krause)
|
| 173 |
+
- Voter models
|
| 174 |
+
- Run iterative influence simulations
|
| 175 |
+
- Detect opinion equilibria and convergence
|
| 176 |
+
- Visualize influence propagation
|
| 177 |
+
|
| 178 |
+
## Extensibility
|
| 179 |
+
|
| 180 |
+
The system is designed for easy extension:
|
| 181 |
+
|
| 182 |
+
**Add Personas:** Drop new JSON files in `data/personas/`
|
| 183 |
+
|
| 184 |
+
**Add Contexts:** Add environmental contexts in `data/contexts/`
|
| 185 |
+
|
| 186 |
+
**Custom LLMs:** Swap `AnthropicClient` for Be.FM or other models
|
| 187 |
+
|
| 188 |
+
**New Features:** Modular architecture supports adding capabilities
|
| 189 |
+
|
| 190 |
+
## Requirements
|
| 191 |
+
|
| 192 |
+
- Python 3.11+
|
| 193 |
+
- Anthropic API key
|
| 194 |
+
- Dependencies in `requirements.txt`
|
| 195 |
+
|
| 196 |
+
## Testing
|
| 197 |
+
|
| 198 |
+
All core functionality tested:
|
| 199 |
+
- β
Persona loading and management
|
| 200 |
+
- β
Context loading and management
|
| 201 |
+
- β
Search and filtering
|
| 202 |
+
- β
Summary generation
|
| 203 |
+
- β
Data validation
|
| 204 |
+
|
| 205 |
+
## Performance
|
| 206 |
+
|
| 207 |
+
- Single query: ~2-5 seconds (depends on LLM)
|
| 208 |
+
- Multi-persona query: Linear scaling
|
| 209 |
+
- Persona loading: Instant (<100ms for 6 personas)
|
| 210 |
+
- Context loading: Instant
|
| 211 |
+
|
| 212 |
+
## Known Limitations
|
| 213 |
+
|
| 214 |
+
1. **LLM Dependency**: Requires Anthropic API access (costs per query)
|
| 215 |
+
2. **No Persistence**: Query history not saved (could add database)
|
| 216 |
+
3. **Static Personas**: Personas don't learn or change (by design for Phase 1)
|
| 217 |
+
4. **English Only**: Currently English language only
|
| 218 |
+
5. **Single Context**: Only one sample context included
|
| 219 |
+
|
| 220 |
+
## Future Enhancements (Beyond Phase 3)
|
| 221 |
+
|
| 222 |
+
- Web interface for broader accessibility
|
| 223 |
+
- Query history and analytics
|
| 224 |
+
- Persona evolution over time
|
| 225 |
+
- Multi-language support
|
| 226 |
+
- Integration with GIS data
|
| 227 |
+
- Real-time context updates
|
| 228 |
+
- Collaborative features for planning teams
|
| 229 |
+
|
| 230 |
+
## Success Metrics
|
| 231 |
+
|
| 232 |
+
Phase 1 successfully delivers:
|
| 233 |
+
- β
Working end-to-end system
|
| 234 |
+
- β
6 diverse, realistic personas
|
| 235 |
+
- β
Contextually-aware responses
|
| 236 |
+
- β
Multiple query interfaces
|
| 237 |
+
- β
Extensible architecture
|
| 238 |
+
- β
Comprehensive documentation
|
| 239 |
+
|
| 240 |
+
## Getting Help
|
| 241 |
+
|
| 242 |
+
- See [GETTING_STARTED.md](GETTING_STARTED.md) for setup instructions
|
| 243 |
+
- Review [README.md](../README.md) for project overview
|
| 244 |
+
- Check example scripts in `examples/` directory
|
| 245 |
+
- Run tests with `python tests/test_basic_functionality.py`
|
| 246 |
+
|
| 247 |
+
---
|
| 248 |
+
|
| 249 |
+
**Phase 1 Status:** β
**COMPLETE**
|
| 250 |
+
|
| 251 |
+
Ready for Phase 2 development and real-world testing!
|
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Phase 1 Example: Multiple Persona Perspectives
|
| 3 |
+
|
| 4 |
+
This example demonstrates querying multiple personas about the same
|
| 5 |
+
urban planning issue to see diverse stakeholder perspectives.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from src.pipeline.query_engine import QueryEngine
|
| 9 |
+
from rich.console import Console
|
| 10 |
+
from rich.panel import Panel
|
| 11 |
+
from rich.markdown import Markdown
|
| 12 |
+
|
| 13 |
+
console = Console()
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def main():
|
| 17 |
+
console.print("\n[bold cyan]Phase 1 Example: Multiple Stakeholder Perspectives[/bold cyan]\n")
|
| 18 |
+
|
| 19 |
+
# Initialize the query engine
|
| 20 |
+
console.print("Initializing system...", style="dim")
|
| 21 |
+
engine = QueryEngine()
|
| 22 |
+
|
| 23 |
+
# Test system
|
| 24 |
+
if not engine.test_system():
|
| 25 |
+
console.print("[red]System test failed. Please check your configuration.[/red]")
|
| 26 |
+
return
|
| 27 |
+
|
| 28 |
+
console.print()
|
| 29 |
+
|
| 30 |
+
# Define a planning scenario
|
| 31 |
+
question = "What's your opinion on the proposed bike lane on Main Street?"
|
| 32 |
+
|
| 33 |
+
scenario = """
|
| 34 |
+
The city is proposing to convert one car lane on Main Street into a
|
| 35 |
+
protected bike lane. This would reduce car lanes from 4 to 3, add
|
| 36 |
+
bike infrastructure, and potentially remove some on-street parking.
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
console.print(Panel(
|
| 40 |
+
f"[bold]Question:[/bold] {question}\n\n"
|
| 41 |
+
f"[bold]Scenario:[/bold]{scenario}",
|
| 42 |
+
title="Planning Issue",
|
| 43 |
+
border_style="cyan"
|
| 44 |
+
))
|
| 45 |
+
|
| 46 |
+
# Query multiple personas
|
| 47 |
+
personas_to_query = [
|
| 48 |
+
"sarah_chen", # Progressive urban planner
|
| 49 |
+
"marcus_thompson", # Business owner
|
| 50 |
+
"elena_rodriguez", # Transportation engineer
|
| 51 |
+
"james_obrien", # Long-time resident
|
| 52 |
+
"priya_patel", # Housing advocate
|
| 53 |
+
"david_kim", # Developer
|
| 54 |
+
]
|
| 55 |
+
|
| 56 |
+
console.print(f"\n[bold]Querying {len(personas_to_query)} stakeholders...[/bold]\n")
|
| 57 |
+
|
| 58 |
+
responses = engine.query_multiple(
|
| 59 |
+
persona_ids=personas_to_query,
|
| 60 |
+
question=question,
|
| 61 |
+
context_id="downtown_district",
|
| 62 |
+
scenario_description=scenario.strip(),
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
# Display all responses
|
| 66 |
+
for i, response in enumerate(responses, 1):
|
| 67 |
+
console.print(f"\n[bold cyan]{i}. {response.persona_name}[/bold cyan] [dim]({response.persona_role})[/dim]")
|
| 68 |
+
console.print("-" * 70)
|
| 69 |
+
console.print(response.response)
|
| 70 |
+
console.print()
|
| 71 |
+
|
| 72 |
+
# Summary
|
| 73 |
+
console.print("\n" + "=" * 70)
|
| 74 |
+
console.print(f"\n[bold green]β Received {len(responses)} diverse perspectives[/bold green]")
|
| 75 |
+
console.print("\n[dim]This demonstrates how different stakeholders view the same ")
|
| 76 |
+
console.print("urban planning issue through their unique lenses of values, ")
|
| 77 |
+
console.print("experience, and priorities.[/dim]\n")
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
if __name__ == "__main__":
|
| 81 |
+
main()
|
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Phase 1 Example: Simple single persona query
|
| 3 |
+
|
| 4 |
+
This example demonstrates the basic functionality of querying
|
| 5 |
+
a single persona about an urban planning topic.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from src.pipeline.query_engine import QueryEngine
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def main():
|
| 12 |
+
print("=" * 70)
|
| 13 |
+
print("Phase 1 Example: Single Persona Query")
|
| 14 |
+
print("=" * 70)
|
| 15 |
+
print()
|
| 16 |
+
|
| 17 |
+
# Initialize the query engine
|
| 18 |
+
print("Initializing system...")
|
| 19 |
+
engine = QueryEngine()
|
| 20 |
+
|
| 21 |
+
# Test system
|
| 22 |
+
print("\nTesting system components...")
|
| 23 |
+
if not engine.test_system():
|
| 24 |
+
print("System test failed. Please check your configuration.")
|
| 25 |
+
return
|
| 26 |
+
|
| 27 |
+
print("\n" + "=" * 70)
|
| 28 |
+
print()
|
| 29 |
+
|
| 30 |
+
# Define a planning question
|
| 31 |
+
question = "What do you think about the proposed bike lane on Main Street?"
|
| 32 |
+
|
| 33 |
+
# Query Sarah Chen (the progressive urban planner)
|
| 34 |
+
print(f"Question: {question}")
|
| 35 |
+
print("\nQuerying persona: Sarah Chen (Urban Planner)")
|
| 36 |
+
print("-" * 70)
|
| 37 |
+
|
| 38 |
+
response = engine.query(
|
| 39 |
+
persona_id="sarah_chen",
|
| 40 |
+
question=question,
|
| 41 |
+
context_id="downtown_district",
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
print(f"\n{response.persona_name} responds:")
|
| 45 |
+
print(f"\n{response.response}")
|
| 46 |
+
print()
|
| 47 |
+
print("-" * 70)
|
| 48 |
+
print(f"Generated at: {response.timestamp}")
|
| 49 |
+
print(f"Model: {response.model_used}")
|
| 50 |
+
print()
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
if __name__ == "__main__":
|
| 54 |
+
main()
|
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core dependencies
|
| 2 |
+
python-dotenv==1.0.0
|
| 3 |
+
pydantic==2.5.0
|
| 4 |
+
pydantic-settings==2.1.0
|
| 5 |
+
|
| 6 |
+
# LLM Integration
|
| 7 |
+
anthropic==0.18.1
|
| 8 |
+
|
| 9 |
+
# API Framework
|
| 10 |
+
fastapi==0.109.0
|
| 11 |
+
uvicorn[standard]==0.27.0
|
| 12 |
+
|
| 13 |
+
# Database
|
| 14 |
+
sqlalchemy==2.0.25
|
| 15 |
+
alembic==1.13.1
|
| 16 |
+
|
| 17 |
+
# Data processing
|
| 18 |
+
numpy==1.26.3
|
| 19 |
+
pandas==2.2.0
|
| 20 |
+
|
| 21 |
+
# Phase 2: Statistical analysis
|
| 22 |
+
scipy==1.12.0
|
| 23 |
+
|
| 24 |
+
# Phase 3: Graph/Network analysis
|
| 25 |
+
networkx==3.2.1
|
| 26 |
+
|
| 27 |
+
# CLI
|
| 28 |
+
click==8.1.7
|
| 29 |
+
rich==13.7.0
|
| 30 |
+
|
| 31 |
+
# Testing
|
| 32 |
+
pytest==8.0.0
|
| 33 |
+
pytest-asyncio==0.23.4
|
| 34 |
+
|
| 35 |
+
# Development
|
| 36 |
+
black==24.1.1
|
| 37 |
+
flake8==7.0.0
|
| 38 |
+
mypy==1.8.0
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AI Personas for Urban Planning System"""
|
| 2 |
+
|
| 3 |
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Interactive CLI for querying personas about urban planning topics
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import sys
|
| 6 |
+
from typing import Optional
|
| 7 |
+
from rich.console import Console
|
| 8 |
+
from rich.prompt import Prompt, Confirm
|
| 9 |
+
from rich.table import Table
|
| 10 |
+
from rich.panel import Panel
|
| 11 |
+
|
| 12 |
+
from src.pipeline.query_engine import QueryEngine
|
| 13 |
+
|
| 14 |
+
console = Console()
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class PersonaCLI:
|
| 18 |
+
"""Interactive command-line interface for persona queries"""
|
| 19 |
+
|
| 20 |
+
def __init__(self):
|
| 21 |
+
"""Initialize CLI"""
|
| 22 |
+
self.engine: Optional[QueryEngine] = None
|
| 23 |
+
|
| 24 |
+
def initialize(self) -> bool:
|
| 25 |
+
"""Initialize the query engine"""
|
| 26 |
+
try:
|
| 27 |
+
console.print("\n[cyan]Initializing AI Personas system...[/cyan]\n")
|
| 28 |
+
self.engine = QueryEngine()
|
| 29 |
+
|
| 30 |
+
if not self.engine.test_system():
|
| 31 |
+
console.print("[red]System initialization failed.[/red]")
|
| 32 |
+
return False
|
| 33 |
+
|
| 34 |
+
console.print("[green]β System ready![/green]\n")
|
| 35 |
+
return True
|
| 36 |
+
|
| 37 |
+
except Exception as e:
|
| 38 |
+
console.print(f"[red]Error initializing system: {e}[/red]")
|
| 39 |
+
return False
|
| 40 |
+
|
| 41 |
+
def show_personas(self):
|
| 42 |
+
"""Display available personas"""
|
| 43 |
+
personas = self.engine.list_available_personas()
|
| 44 |
+
|
| 45 |
+
table = Table(title="Available Personas", show_header=True)
|
| 46 |
+
table.add_column("ID", style="cyan")
|
| 47 |
+
table.add_column("Name", style="green")
|
| 48 |
+
table.add_column("Role", style="yellow")
|
| 49 |
+
|
| 50 |
+
for persona_id, name, role in personas:
|
| 51 |
+
table.add_row(persona_id, name, role)
|
| 52 |
+
|
| 53 |
+
console.print(table)
|
| 54 |
+
console.print()
|
| 55 |
+
|
| 56 |
+
def show_contexts(self):
|
| 57 |
+
"""Display available contexts"""
|
| 58 |
+
contexts = self.engine.list_available_contexts()
|
| 59 |
+
|
| 60 |
+
if not contexts:
|
| 61 |
+
console.print("[yellow]No environmental contexts loaded.[/yellow]\n")
|
| 62 |
+
return
|
| 63 |
+
|
| 64 |
+
console.print("[cyan]Available Contexts:[/cyan]")
|
| 65 |
+
for context_id in contexts:
|
| 66 |
+
console.print(f" β’ {context_id}")
|
| 67 |
+
console.print()
|
| 68 |
+
|
| 69 |
+
def query_single_persona(self):
|
| 70 |
+
"""Interactive single persona query"""
|
| 71 |
+
# Show available personas
|
| 72 |
+
self.show_personas()
|
| 73 |
+
|
| 74 |
+
# Get persona selection
|
| 75 |
+
personas = self.engine.list_available_personas()
|
| 76 |
+
persona_ids = [p[0] for p in personas]
|
| 77 |
+
|
| 78 |
+
persona_id = Prompt.ask(
|
| 79 |
+
"Select a persona",
|
| 80 |
+
choices=persona_ids,
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
# Get question
|
| 84 |
+
console.print()
|
| 85 |
+
question = Prompt.ask("[cyan]Your question[/cyan]")
|
| 86 |
+
|
| 87 |
+
# Optional context
|
| 88 |
+
console.print()
|
| 89 |
+
use_context = Confirm.ask("Use environmental context?", default=False)
|
| 90 |
+
context_id = None
|
| 91 |
+
|
| 92 |
+
if use_context:
|
| 93 |
+
contexts = self.engine.list_available_contexts()
|
| 94 |
+
if contexts:
|
| 95 |
+
context_id = Prompt.ask(
|
| 96 |
+
"Select context",
|
| 97 |
+
choices=contexts,
|
| 98 |
+
default=contexts[0] if contexts else None,
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
# Query
|
| 102 |
+
console.print("\n[dim]Generating response...[/dim]\n")
|
| 103 |
+
|
| 104 |
+
try:
|
| 105 |
+
response = self.engine.query(
|
| 106 |
+
persona_id=persona_id,
|
| 107 |
+
question=question,
|
| 108 |
+
context_id=context_id,
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
# Display response
|
| 112 |
+
console.print(Panel(
|
| 113 |
+
f"[bold]{response.persona_name}[/bold] [dim]({response.persona_role})[/dim]\n\n"
|
| 114 |
+
f"{response.response}",
|
| 115 |
+
title="Response",
|
| 116 |
+
border_style="green",
|
| 117 |
+
))
|
| 118 |
+
console.print()
|
| 119 |
+
|
| 120 |
+
except Exception as e:
|
| 121 |
+
console.print(f"[red]Error: {e}[/red]\n")
|
| 122 |
+
|
| 123 |
+
def query_multiple_personas(self):
|
| 124 |
+
"""Query multiple personas with the same question"""
|
| 125 |
+
# Show available personas
|
| 126 |
+
self.show_personas()
|
| 127 |
+
|
| 128 |
+
# Get question
|
| 129 |
+
question = Prompt.ask("[cyan]Your question (will be asked to all personas)[/cyan]")
|
| 130 |
+
|
| 131 |
+
# Optional context
|
| 132 |
+
console.print()
|
| 133 |
+
use_context = Confirm.ask("Use environmental context?", default=False)
|
| 134 |
+
context_id = None
|
| 135 |
+
|
| 136 |
+
if use_context:
|
| 137 |
+
contexts = self.engine.list_available_contexts()
|
| 138 |
+
if contexts:
|
| 139 |
+
context_id = Prompt.ask(
|
| 140 |
+
"Select context",
|
| 141 |
+
choices=contexts,
|
| 142 |
+
default=contexts[0] if contexts else None,
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
# Query all personas
|
| 146 |
+
personas = self.engine.list_available_personas()
|
| 147 |
+
persona_ids = [p[0] for p in personas]
|
| 148 |
+
|
| 149 |
+
console.print(f"\n[dim]Querying {len(persona_ids)} personas...[/dim]\n")
|
| 150 |
+
|
| 151 |
+
try:
|
| 152 |
+
responses = self.engine.query_multiple(
|
| 153 |
+
persona_ids=persona_ids,
|
| 154 |
+
question=question,
|
| 155 |
+
context_id=context_id,
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
# Display all responses
|
| 159 |
+
for i, response in enumerate(responses, 1):
|
| 160 |
+
console.print(f"[bold cyan]{i}. {response.persona_name}[/bold cyan] [dim]({response.persona_role})[/dim]")
|
| 161 |
+
console.print("-" * 70)
|
| 162 |
+
console.print(response.response)
|
| 163 |
+
console.print()
|
| 164 |
+
|
| 165 |
+
console.print(f"[green]β Received {len(responses)} responses[/green]\n")
|
| 166 |
+
|
| 167 |
+
except Exception as e:
|
| 168 |
+
console.print(f"[red]Error: {e}[/red]\n")
|
| 169 |
+
|
| 170 |
+
def show_help(self):
|
| 171 |
+
"""Show help information"""
|
| 172 |
+
help_text = """
|
| 173 |
+
[bold cyan]AI Personas for Urban Planning - Interactive CLI[/bold cyan]
|
| 174 |
+
|
| 175 |
+
[bold]Commands:[/bold]
|
| 176 |
+
1 - Query a single persona
|
| 177 |
+
2 - Query all personas with the same question
|
| 178 |
+
3 - List available personas
|
| 179 |
+
4 - List available contexts
|
| 180 |
+
h - Show this help
|
| 181 |
+
q - Quit
|
| 182 |
+
|
| 183 |
+
[bold]About:[/bold]
|
| 184 |
+
This system allows you to query synthetic personas representing different
|
| 185 |
+
urban planning stakeholders. Each persona has unique values, experiences,
|
| 186 |
+
and perspectives that shape their responses.
|
| 187 |
+
"""
|
| 188 |
+
console.print(Panel(help_text.strip(), border_style="cyan"))
|
| 189 |
+
console.print()
|
| 190 |
+
|
| 191 |
+
def run(self):
|
| 192 |
+
"""Run the interactive CLI"""
|
| 193 |
+
if not self.initialize():
|
| 194 |
+
return
|
| 195 |
+
|
| 196 |
+
self.show_help()
|
| 197 |
+
|
| 198 |
+
while True:
|
| 199 |
+
console.print("[cyan]Options:[/cyan] [1]Single [2]Multiple [3]List Personas [4]List Contexts [h]Help [q]Quit")
|
| 200 |
+
choice = Prompt.ask("Select", default="1")
|
| 201 |
+
|
| 202 |
+
if choice.lower() in ["q", "quit", "exit"]:
|
| 203 |
+
console.print("\n[cyan]Goodbye![/cyan]\n")
|
| 204 |
+
break
|
| 205 |
+
|
| 206 |
+
elif choice == "1":
|
| 207 |
+
console.print()
|
| 208 |
+
self.query_single_persona()
|
| 209 |
+
|
| 210 |
+
elif choice == "2":
|
| 211 |
+
console.print()
|
| 212 |
+
self.query_multiple_personas()
|
| 213 |
+
|
| 214 |
+
elif choice == "3":
|
| 215 |
+
console.print()
|
| 216 |
+
self.show_personas()
|
| 217 |
+
|
| 218 |
+
elif choice == "4":
|
| 219 |
+
console.print()
|
| 220 |
+
self.show_contexts()
|
| 221 |
+
|
| 222 |
+
elif choice.lower() in ["h", "help"]:
|
| 223 |
+
console.print()
|
| 224 |
+
self.show_help()
|
| 225 |
+
|
| 226 |
+
else:
|
| 227 |
+
console.print("[yellow]Invalid choice. Press 'h' for help.[/yellow]\n")
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def main():
|
| 231 |
+
"""Main entry point"""
|
| 232 |
+
try:
|
| 233 |
+
cli = PersonaCLI()
|
| 234 |
+
cli.run()
|
| 235 |
+
except KeyboardInterrupt:
|
| 236 |
+
console.print("\n\n[cyan]Interrupted. Goodbye![/cyan]\n")
|
| 237 |
+
sys.exit(0)
|
| 238 |
+
except Exception as e:
|
| 239 |
+
console.print(f"\n[red]Error: {e}[/red]\n")
|
| 240 |
+
sys.exit(1)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
if __name__ == "__main__":
|
| 244 |
+
main()
|
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Environmental and built environment context system"""
|
| 2 |
+
|
| 3 |
+
from .models import (
|
| 4 |
+
BuiltEnvironment,
|
| 5 |
+
SocialContext,
|
| 6 |
+
TemporalContext,
|
| 7 |
+
EconomicContext,
|
| 8 |
+
EnvironmentalContext,
|
| 9 |
+
)
|
| 10 |
+
from .database import ContextDatabase
|
| 11 |
+
|
| 12 |
+
__all__ = [
|
| 13 |
+
"BuiltEnvironment",
|
| 14 |
+
"SocialContext",
|
| 15 |
+
"TemporalContext",
|
| 16 |
+
"EconomicContext",
|
| 17 |
+
"EnvironmentalContext",
|
| 18 |
+
"ContextDatabase",
|
| 19 |
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Context database and management system"""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Dict, List, Optional
|
| 6 |
+
from .models import EnvironmentalContext
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class ContextDatabase:
|
| 10 |
+
"""Database for managing environmental contexts"""
|
| 11 |
+
|
| 12 |
+
def __init__(self, contexts_dir: Optional[Path] = None):
|
| 13 |
+
"""
|
| 14 |
+
Initialize context database
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
contexts_dir: Directory containing context JSON files.
|
| 18 |
+
Defaults to data/contexts relative to project root.
|
| 19 |
+
"""
|
| 20 |
+
if contexts_dir is None:
|
| 21 |
+
# Default to data/contexts from project root
|
| 22 |
+
project_root = Path(__file__).parent.parent.parent
|
| 23 |
+
contexts_dir = project_root / "data" / "contexts"
|
| 24 |
+
|
| 25 |
+
self.contexts_dir = Path(contexts_dir)
|
| 26 |
+
self.contexts: Dict[str, EnvironmentalContext] = {}
|
| 27 |
+
self._load_contexts()
|
| 28 |
+
|
| 29 |
+
def _load_contexts(self) -> None:
|
| 30 |
+
"""Load all context JSON files from the contexts directory"""
|
| 31 |
+
if not self.contexts_dir.exists():
|
| 32 |
+
print(f"Warning: Contexts directory not found: {self.contexts_dir}")
|
| 33 |
+
print("Creating empty contexts directory...")
|
| 34 |
+
self.contexts_dir.mkdir(parents=True, exist_ok=True)
|
| 35 |
+
return
|
| 36 |
+
|
| 37 |
+
json_files = list(self.contexts_dir.glob("*.json"))
|
| 38 |
+
if not json_files:
|
| 39 |
+
print(f"Warning: No context JSON files found in {self.contexts_dir}")
|
| 40 |
+
return
|
| 41 |
+
|
| 42 |
+
for json_file in json_files:
|
| 43 |
+
try:
|
| 44 |
+
with open(json_file, "r", encoding="utf-8") as f:
|
| 45 |
+
data = json.load(f)
|
| 46 |
+
context = EnvironmentalContext(**data)
|
| 47 |
+
self.contexts[context.context_id] = context
|
| 48 |
+
except Exception as e:
|
| 49 |
+
print(f"Warning: Failed to load {json_file}: {e}")
|
| 50 |
+
|
| 51 |
+
print(f"Loaded {len(self.contexts)} environmental contexts")
|
| 52 |
+
|
| 53 |
+
def get_context(self, context_id: str) -> Optional[EnvironmentalContext]:
|
| 54 |
+
"""
|
| 55 |
+
Retrieve a context by ID
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
context_id: Unique context identifier
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
EnvironmentalContext object if found, None otherwise
|
| 62 |
+
"""
|
| 63 |
+
return self.contexts.get(context_id)
|
| 64 |
+
|
| 65 |
+
def get_all_contexts(self) -> List[EnvironmentalContext]:
|
| 66 |
+
"""
|
| 67 |
+
Get all loaded contexts
|
| 68 |
+
|
| 69 |
+
Returns:
|
| 70 |
+
List of all EnvironmentalContext objects
|
| 71 |
+
"""
|
| 72 |
+
return list(self.contexts.values())
|
| 73 |
+
|
| 74 |
+
def list_context_ids(self) -> List[str]:
|
| 75 |
+
"""
|
| 76 |
+
Get list of all context IDs
|
| 77 |
+
|
| 78 |
+
Returns:
|
| 79 |
+
List of context ID strings
|
| 80 |
+
"""
|
| 81 |
+
return list(self.contexts.keys())
|
| 82 |
+
|
| 83 |
+
def get_default_context(self) -> Optional[EnvironmentalContext]:
|
| 84 |
+
"""
|
| 85 |
+
Get a default context (first available)
|
| 86 |
+
|
| 87 |
+
Returns:
|
| 88 |
+
First EnvironmentalContext or None if none exist
|
| 89 |
+
"""
|
| 90 |
+
if self.contexts:
|
| 91 |
+
return list(self.contexts.values())[0]
|
| 92 |
+
return None
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def load_context_from_file(filepath: Path) -> EnvironmentalContext:
|
| 96 |
+
"""
|
| 97 |
+
Load a single context from a JSON file
|
| 98 |
+
|
| 99 |
+
Args:
|
| 100 |
+
filepath: Path to JSON file
|
| 101 |
+
|
| 102 |
+
Returns:
|
| 103 |
+
EnvironmentalContext object
|
| 104 |
+
"""
|
| 105 |
+
with open(filepath, "r", encoding="utf-8") as f:
|
| 106 |
+
data = json.load(f)
|
| 107 |
+
return EnvironmentalContext(**data)
|
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Environmental and contextual data models for urban planning"""
|
| 2 |
+
|
| 3 |
+
from typing import List, Dict, Optional, Any
|
| 4 |
+
from pydantic import BaseModel, Field
|
| 5 |
+
from enum import Enum
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class AreaType(str, Enum):
|
| 10 |
+
"""Urban area classification"""
|
| 11 |
+
DOWNTOWN = "downtown"
|
| 12 |
+
URBAN_NEIGHBORHOOD = "urban_neighborhood"
|
| 13 |
+
SUBURBAN = "suburban"
|
| 14 |
+
RURAL = "rural"
|
| 15 |
+
MIXED_USE = "mixed_use"
|
| 16 |
+
INDUSTRIAL = "industrial"
|
| 17 |
+
COMMERCIAL_CORRIDOR = "commercial_corridor"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class TransitAccessLevel(str, Enum):
|
| 21 |
+
"""Transit accessibility levels"""
|
| 22 |
+
EXCELLENT = "excellent" # <5 min walk to frequent transit
|
| 23 |
+
GOOD = "good" # 5-10 min walk to frequent transit
|
| 24 |
+
MODERATE = "moderate" # 10-15 min walk or infrequent service
|
| 25 |
+
LIMITED = "limited" # 15+ min walk or very infrequent
|
| 26 |
+
NONE = "none" # No transit access
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class BuiltEnvironment(BaseModel):
|
| 30 |
+
"""Physical built environment characteristics"""
|
| 31 |
+
|
| 32 |
+
location_id: str = Field(..., description="Unique location identifier")
|
| 33 |
+
name: str = Field(..., description="Location name")
|
| 34 |
+
area_type: AreaType = Field(..., description="Type of urban area")
|
| 35 |
+
|
| 36 |
+
# Density and land use
|
| 37 |
+
population_density: int = Field(
|
| 38 |
+
..., description="People per square mile"
|
| 39 |
+
)
|
| 40 |
+
housing_density: int = Field(
|
| 41 |
+
..., description="Housing units per square mile"
|
| 42 |
+
)
|
| 43 |
+
land_use_mix: float = Field(
|
| 44 |
+
..., ge=0, le=1,
|
| 45 |
+
description="Mixed use score (0=single use, 1=highly mixed)"
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
# Transportation infrastructure
|
| 49 |
+
transit_access: TransitAccessLevel = Field(
|
| 50 |
+
..., description="Public transit accessibility"
|
| 51 |
+
)
|
| 52 |
+
bike_infrastructure: int = Field(
|
| 53 |
+
..., ge=1, le=10,
|
| 54 |
+
description="Quality of bike infrastructure (1-10)"
|
| 55 |
+
)
|
| 56 |
+
sidewalk_coverage: float = Field(
|
| 57 |
+
..., ge=0, le=1,
|
| 58 |
+
description="Percentage of streets with sidewalks"
|
| 59 |
+
)
|
| 60 |
+
parking_availability: int = Field(
|
| 61 |
+
..., ge=1, le=10,
|
| 62 |
+
description="Parking availability (1-10)"
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
# Amenities and services
|
| 66 |
+
walkability_score: int = Field(
|
| 67 |
+
..., ge=0, le=100,
|
| 68 |
+
description="Walk Score (0-100)"
|
| 69 |
+
)
|
| 70 |
+
parks_access: int = Field(
|
| 71 |
+
..., ge=1, le=10,
|
| 72 |
+
description="Access to parks and green space (1-10)"
|
| 73 |
+
)
|
| 74 |
+
retail_access: int = Field(
|
| 75 |
+
..., ge=1, le=10,
|
| 76 |
+
description="Access to retail and services (1-10)"
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
# Building characteristics
|
| 80 |
+
building_age: str = Field(
|
| 81 |
+
..., description="Predominant building age (e.g., 'pre-1950', '1950-2000', 'post-2000')"
|
| 82 |
+
)
|
| 83 |
+
historic_district: bool = Field(
|
| 84 |
+
default=False, description="Located in historic district"
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
# Infrastructure condition
|
| 88 |
+
infrastructure_condition: int = Field(
|
| 89 |
+
..., ge=1, le=10,
|
| 90 |
+
description="Overall infrastructure condition (1-10)"
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
description: str = Field(
|
| 94 |
+
..., description="Narrative description of the area"
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class SocialContext(BaseModel):
|
| 99 |
+
"""Social and community characteristics"""
|
| 100 |
+
|
| 101 |
+
location_id: str = Field(..., description="Links to BuiltEnvironment location_id")
|
| 102 |
+
|
| 103 |
+
# Demographics
|
| 104 |
+
median_age: int = Field(..., ge=0)
|
| 105 |
+
median_income: int = Field(..., ge=0, description="Median household income")
|
| 106 |
+
poverty_rate: float = Field(..., ge=0, le=1, description="Percentage below poverty line")
|
| 107 |
+
|
| 108 |
+
# Diversity
|
| 109 |
+
racial_diversity_index: float = Field(
|
| 110 |
+
..., ge=0, le=1,
|
| 111 |
+
description="Diversity index (0=homogeneous, 1=highly diverse)"
|
| 112 |
+
)
|
| 113 |
+
language_diversity: int = Field(
|
| 114 |
+
..., ge=1, le=10,
|
| 115 |
+
description="Language diversity (1-10)"
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
# Community characteristics
|
| 119 |
+
homeownership_rate: float = Field(
|
| 120 |
+
..., ge=0, le=1,
|
| 121 |
+
description="Percentage of owner-occupied housing"
|
| 122 |
+
)
|
| 123 |
+
resident_stability: float = Field(
|
| 124 |
+
..., ge=0, le=1,
|
| 125 |
+
description="Percentage living in same house 5+ years"
|
| 126 |
+
)
|
| 127 |
+
community_organization_strength: int = Field(
|
| 128 |
+
..., ge=1, le=10,
|
| 129 |
+
description="Strength of community organizations (1-10)"
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
# Recent trends
|
| 133 |
+
gentrification_pressure: int = Field(
|
| 134 |
+
..., ge=1, le=10,
|
| 135 |
+
description="Level of gentrification pressure (1-10)"
|
| 136 |
+
)
|
| 137 |
+
recent_demographic_changes: List[str] = Field(
|
| 138 |
+
default_factory=list,
|
| 139 |
+
description="Notable demographic shifts"
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
cultural_character: str = Field(
|
| 143 |
+
..., description="Description of cultural identity and character"
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
class TemporalContext(BaseModel):
|
| 148 |
+
"""Time-based contextual information"""
|
| 149 |
+
|
| 150 |
+
# Time of day
|
| 151 |
+
time_of_day: str = Field(
|
| 152 |
+
..., description="Time period (morning_rush, midday, evening_rush, night)"
|
| 153 |
+
)
|
| 154 |
+
day_of_week: str = Field(
|
| 155 |
+
..., description="Day of week"
|
| 156 |
+
)
|
| 157 |
+
season: str = Field(
|
| 158 |
+
..., description="Season (spring, summer, fall, winter)"
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
# Recent events
|
| 162 |
+
recent_events: List[str] = Field(
|
| 163 |
+
default_factory=list,
|
| 164 |
+
description="Recent relevant events or meetings"
|
| 165 |
+
)
|
| 166 |
+
upcoming_decisions: List[str] = Field(
|
| 167 |
+
default_factory=list,
|
| 168 |
+
description="Upcoming planning decisions or votes"
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
# Current conditions
|
| 172 |
+
weather: Optional[str] = Field(
|
| 173 |
+
None, description="Current weather conditions"
|
| 174 |
+
)
|
| 175 |
+
special_circumstances: List[str] = Field(
|
| 176 |
+
default_factory=list,
|
| 177 |
+
description="Any special circumstances (construction, events, etc.)"
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
class EconomicContext(BaseModel):
|
| 182 |
+
"""Economic conditions and trends"""
|
| 183 |
+
|
| 184 |
+
location_id: str = Field(..., description="Links to BuiltEnvironment location_id")
|
| 185 |
+
|
| 186 |
+
# Employment
|
| 187 |
+
unemployment_rate: float = Field(..., ge=0, le=1)
|
| 188 |
+
major_employers: List[str] = Field(default_factory=list)
|
| 189 |
+
job_growth_rate: float = Field(..., description="Annual job growth rate")
|
| 190 |
+
|
| 191 |
+
# Business
|
| 192 |
+
small_business_density: int = Field(
|
| 193 |
+
..., ge=1, le=10,
|
| 194 |
+
description="Concentration of small businesses (1-10)"
|
| 195 |
+
)
|
| 196 |
+
commercial_vacancy_rate: float = Field(
|
| 197 |
+
..., ge=0, le=1,
|
| 198 |
+
description="Commercial property vacancy rate"
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
# Housing market
|
| 202 |
+
median_home_price: int = Field(..., ge=0)
|
| 203 |
+
median_rent: int = Field(..., ge=0, description="Median monthly rent")
|
| 204 |
+
housing_cost_burden: float = Field(
|
| 205 |
+
..., ge=0, le=1,
|
| 206 |
+
description="Percentage of households paying >30% income on housing"
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
# Investment
|
| 210 |
+
recent_investment: List[str] = Field(
|
| 211 |
+
default_factory=list,
|
| 212 |
+
description="Recent major investments or developments"
|
| 213 |
+
)
|
| 214 |
+
planned_developments: List[str] = Field(
|
| 215 |
+
default_factory=list,
|
| 216 |
+
description="Planned future developments"
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
economic_trends: str = Field(
|
| 220 |
+
..., description="Narrative description of economic conditions and trends"
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
class EnvironmentalContext(BaseModel):
|
| 225 |
+
"""Complete environmental context combining all dimensions"""
|
| 226 |
+
|
| 227 |
+
context_id: str = Field(..., description="Unique context identifier")
|
| 228 |
+
built_environment: BuiltEnvironment
|
| 229 |
+
social_context: SocialContext
|
| 230 |
+
temporal_context: TemporalContext
|
| 231 |
+
economic_context: EconomicContext
|
| 232 |
+
|
| 233 |
+
# Additional metadata
|
| 234 |
+
metadata: Dict[str, Any] = Field(
|
| 235 |
+
default_factory=dict,
|
| 236 |
+
description="Additional flexible metadata"
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
def get_context_summary(self) -> str:
|
| 240 |
+
"""Generate text summary for LLM context"""
|
| 241 |
+
return f"""
|
| 242 |
+
LOCATION: {self.built_environment.name} ({self.built_environment.area_type})
|
| 243 |
+
|
| 244 |
+
Built Environment:
|
| 245 |
+
- Area type: {self.built_environment.area_type}
|
| 246 |
+
- Population density: {self.built_environment.population_density:,} people/sq mi
|
| 247 |
+
- Transit access: {self.built_environment.transit_access}
|
| 248 |
+
- Walkability: {self.built_environment.walkability_score}/100
|
| 249 |
+
- {self.built_environment.description}
|
| 250 |
+
|
| 251 |
+
Social Context:
|
| 252 |
+
- Median income: ${self.social_context.median_income:,}
|
| 253 |
+
- Homeownership rate: {self.social_context.homeownership_rate:.1%}
|
| 254 |
+
- Gentrification pressure: {self.social_context.gentrification_pressure}/10
|
| 255 |
+
- {self.social_context.cultural_character}
|
| 256 |
+
|
| 257 |
+
Economic Context:
|
| 258 |
+
- Median home price: ${self.economic_context.median_home_price:,}
|
| 259 |
+
- Median rent: ${self.economic_context.median_rent:,}/month
|
| 260 |
+
- {self.economic_context.economic_trends}
|
| 261 |
+
|
| 262 |
+
Temporal Context:
|
| 263 |
+
- Time: {self.temporal_context.time_of_day}, {self.temporal_context.day_of_week}
|
| 264 |
+
- Season: {self.temporal_context.season}
|
| 265 |
+
- Recent events: {', '.join(self.temporal_context.recent_events) if self.temporal_context.recent_events else 'None'}
|
| 266 |
+
""".strip()
|
| 267 |
+
|
| 268 |
+
class Config:
|
| 269 |
+
"""Pydantic config"""
|
| 270 |
+
use_enum_values = True
|
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Phase 3: Multi-persona influence and opinion dynamics
|
| 3 |
+
|
| 4 |
+
This module will enable:
|
| 5 |
+
- Modeling social network graphs between personas
|
| 6 |
+
- Implementing opinion dynamics models (DeGroot, Bounded Confidence, Voter)
|
| 7 |
+
- Running simulations to discover opinion equilibria
|
| 8 |
+
- Visualizing influence propagation and convergence
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
# Phase 3 implementation coming soon
|
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LLM integration modules"""
|
| 2 |
+
|
| 3 |
+
from .anthropic_client import AnthropicClient
|
| 4 |
+
from .prompt_builder import PromptBuilder
|
| 5 |
+
|
| 6 |
+
__all__ = ["AnthropicClient", "PromptBuilder"]
|
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Anthropic Claude API client"""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from typing import Optional, List, Dict, Any
|
| 5 |
+
from anthropic import Anthropic
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
|
| 8 |
+
# Load environment variables
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class AnthropicClient:
|
| 13 |
+
"""Client for interacting with Anthropic Claude API"""
|
| 14 |
+
|
| 15 |
+
def __init__(
|
| 16 |
+
self,
|
| 17 |
+
api_key: Optional[str] = None,
|
| 18 |
+
model: Optional[str] = None,
|
| 19 |
+
max_tokens: int = 2048,
|
| 20 |
+
temperature: float = 0.7,
|
| 21 |
+
):
|
| 22 |
+
"""
|
| 23 |
+
Initialize Anthropic client
|
| 24 |
+
|
| 25 |
+
Args:
|
| 26 |
+
api_key: Anthropic API key (defaults to ANTHROPIC_API_KEY env var)
|
| 27 |
+
model: Model name (defaults to LLM_MODEL env var or claude-3-5-sonnet)
|
| 28 |
+
max_tokens: Maximum tokens in response
|
| 29 |
+
temperature: Sampling temperature (0-1)
|
| 30 |
+
"""
|
| 31 |
+
self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
|
| 32 |
+
if not self.api_key:
|
| 33 |
+
raise ValueError(
|
| 34 |
+
"Anthropic API key must be provided or set in ANTHROPIC_API_KEY env var"
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
self.model = model or os.getenv(
|
| 38 |
+
"LLM_MODEL", "claude-3-5-sonnet-20241022"
|
| 39 |
+
)
|
| 40 |
+
self.max_tokens = int(os.getenv("LLM_MAX_TOKENS", max_tokens))
|
| 41 |
+
self.temperature = float(os.getenv("LLM_TEMPERATURE", temperature))
|
| 42 |
+
|
| 43 |
+
self.client = Anthropic(api_key=self.api_key)
|
| 44 |
+
|
| 45 |
+
def generate_response(
|
| 46 |
+
self,
|
| 47 |
+
system_prompt: str,
|
| 48 |
+
user_message: str,
|
| 49 |
+
temperature: Optional[float] = None,
|
| 50 |
+
max_tokens: Optional[int] = None,
|
| 51 |
+
) -> str:
|
| 52 |
+
"""
|
| 53 |
+
Generate a response from Claude
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
system_prompt: System prompt defining persona and context
|
| 57 |
+
user_message: User's question or input
|
| 58 |
+
temperature: Override default temperature
|
| 59 |
+
max_tokens: Override default max_tokens
|
| 60 |
+
|
| 61 |
+
Returns:
|
| 62 |
+
Generated response text
|
| 63 |
+
"""
|
| 64 |
+
try:
|
| 65 |
+
message = self.client.messages.create(
|
| 66 |
+
model=self.model,
|
| 67 |
+
max_tokens=max_tokens or self.max_tokens,
|
| 68 |
+
temperature=temperature or self.temperature,
|
| 69 |
+
system=system_prompt,
|
| 70 |
+
messages=[
|
| 71 |
+
{
|
| 72 |
+
"role": "user",
|
| 73 |
+
"content": user_message,
|
| 74 |
+
}
|
| 75 |
+
],
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
# Extract text from response
|
| 79 |
+
response_text = message.content[0].text
|
| 80 |
+
return response_text
|
| 81 |
+
|
| 82 |
+
except Exception as e:
|
| 83 |
+
raise Exception(f"Error generating response from Claude: {e}")
|
| 84 |
+
|
| 85 |
+
def generate_response_with_history(
|
| 86 |
+
self,
|
| 87 |
+
system_prompt: str,
|
| 88 |
+
conversation_history: List[Dict[str, str]],
|
| 89 |
+
temperature: Optional[float] = None,
|
| 90 |
+
max_tokens: Optional[int] = None,
|
| 91 |
+
) -> str:
|
| 92 |
+
"""
|
| 93 |
+
Generate a response with conversation history
|
| 94 |
+
|
| 95 |
+
Args:
|
| 96 |
+
system_prompt: System prompt defining persona and context
|
| 97 |
+
conversation_history: List of {"role": "user"|"assistant", "content": str}
|
| 98 |
+
temperature: Override default temperature
|
| 99 |
+
max_tokens: Override default max_tokens
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
Generated response text
|
| 103 |
+
"""
|
| 104 |
+
try:
|
| 105 |
+
message = self.client.messages.create(
|
| 106 |
+
model=self.model,
|
| 107 |
+
max_tokens=max_tokens or self.max_tokens,
|
| 108 |
+
temperature=temperature or self.temperature,
|
| 109 |
+
system=system_prompt,
|
| 110 |
+
messages=conversation_history,
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
response_text = message.content[0].text
|
| 114 |
+
return response_text
|
| 115 |
+
|
| 116 |
+
except Exception as e:
|
| 117 |
+
raise Exception(f"Error generating response from Claude: {e}")
|
| 118 |
+
|
| 119 |
+
def test_connection(self) -> bool:
|
| 120 |
+
"""
|
| 121 |
+
Test the API connection
|
| 122 |
+
|
| 123 |
+
Returns:
|
| 124 |
+
True if connection successful, False otherwise
|
| 125 |
+
"""
|
| 126 |
+
try:
|
| 127 |
+
response = self.generate_response(
|
| 128 |
+
system_prompt="You are a helpful assistant.",
|
| 129 |
+
user_message="Hello, respond with 'OK' if you can hear me.",
|
| 130 |
+
max_tokens=10,
|
| 131 |
+
)
|
| 132 |
+
return "OK" in response or "ok" in response.lower()
|
| 133 |
+
except Exception as e:
|
| 134 |
+
print(f"Connection test failed: {e}")
|
| 135 |
+
return False
|
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Prompt construction for persona-based responses"""
|
| 2 |
+
|
| 3 |
+
from typing import Optional
|
| 4 |
+
from ..personas.models import Persona
|
| 5 |
+
from ..context.models import EnvironmentalContext
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class PromptBuilder:
|
| 9 |
+
"""Build system prompts for persona-based LLM queries"""
|
| 10 |
+
|
| 11 |
+
@staticmethod
|
| 12 |
+
def build_persona_system_prompt(
|
| 13 |
+
persona: Persona,
|
| 14 |
+
context: Optional[EnvironmentalContext] = None,
|
| 15 |
+
additional_instructions: Optional[str] = None,
|
| 16 |
+
) -> str:
|
| 17 |
+
"""
|
| 18 |
+
Build a system prompt that embodies a persona
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
persona: Persona object to embody
|
| 22 |
+
context: Optional environmental context
|
| 23 |
+
additional_instructions: Optional additional instructions
|
| 24 |
+
|
| 25 |
+
Returns:
|
| 26 |
+
System prompt string
|
| 27 |
+
"""
|
| 28 |
+
prompt_parts = [
|
| 29 |
+
"You are responding as the following person in an urban planning context:",
|
| 30 |
+
"",
|
| 31 |
+
persona.get_context_summary(),
|
| 32 |
+
"",
|
| 33 |
+
"IMPORTANT INSTRUCTIONS:",
|
| 34 |
+
"- Respond authentically as this person would, reflecting their:",
|
| 35 |
+
" * Values, priorities, and political orientation",
|
| 36 |
+
" * Communication style and language patterns",
|
| 37 |
+
" * Professional expertise and life experiences",
|
| 38 |
+
" * Typical concerns and decision-making approach",
|
| 39 |
+
"- Use first-person perspective ('I think...', 'In my experience...')",
|
| 40 |
+
"- Be specific and grounded in this persona's background",
|
| 41 |
+
"- Show the nuance and complexity of real people",
|
| 42 |
+
"- You may disagree with or question aspects of proposals",
|
| 43 |
+
"- Reference your lived experience and expertise where relevant",
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
if context:
|
| 47 |
+
prompt_parts.extend([
|
| 48 |
+
"",
|
| 49 |
+
"CURRENT CONTEXT:",
|
| 50 |
+
context.get_context_summary(),
|
| 51 |
+
"",
|
| 52 |
+
"Consider how these environmental conditions might influence your perspective.",
|
| 53 |
+
])
|
| 54 |
+
|
| 55 |
+
if additional_instructions:
|
| 56 |
+
prompt_parts.extend([
|
| 57 |
+
"",
|
| 58 |
+
"ADDITIONAL GUIDANCE:",
|
| 59 |
+
additional_instructions,
|
| 60 |
+
])
|
| 61 |
+
|
| 62 |
+
prompt_parts.extend([
|
| 63 |
+
"",
|
| 64 |
+
"Respond thoughtfully and authentically as this persona.",
|
| 65 |
+
])
|
| 66 |
+
|
| 67 |
+
return "\n".join(prompt_parts)
|
| 68 |
+
|
| 69 |
+
@staticmethod
|
| 70 |
+
def build_simple_query(question: str) -> str:
|
| 71 |
+
"""
|
| 72 |
+
Build a simple user query
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
question: The question to ask
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
Formatted user message
|
| 79 |
+
"""
|
| 80 |
+
return question
|
| 81 |
+
|
| 82 |
+
@staticmethod
|
| 83 |
+
def build_contextual_query(
|
| 84 |
+
question: str,
|
| 85 |
+
scenario_description: Optional[str] = None,
|
| 86 |
+
specific_context: Optional[str] = None,
|
| 87 |
+
) -> str:
|
| 88 |
+
"""
|
| 89 |
+
Build a query with additional contextual information
|
| 90 |
+
|
| 91 |
+
Args:
|
| 92 |
+
question: The main question
|
| 93 |
+
scenario_description: Optional scenario context
|
| 94 |
+
specific_context: Optional specific situational details
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
Formatted user message with context
|
| 98 |
+
"""
|
| 99 |
+
parts = []
|
| 100 |
+
|
| 101 |
+
if scenario_description:
|
| 102 |
+
parts.append(f"SCENARIO: {scenario_description}")
|
| 103 |
+
parts.append("")
|
| 104 |
+
|
| 105 |
+
if specific_context:
|
| 106 |
+
parts.append(f"CONTEXT: {specific_context}")
|
| 107 |
+
parts.append("")
|
| 108 |
+
|
| 109 |
+
parts.append(question)
|
| 110 |
+
|
| 111 |
+
return "\n".join(parts)
|
| 112 |
+
|
| 113 |
+
@staticmethod
|
| 114 |
+
def build_comparison_prompt(
|
| 115 |
+
personas: list[Persona],
|
| 116 |
+
question: str,
|
| 117 |
+
) -> str:
|
| 118 |
+
"""
|
| 119 |
+
Build a prompt for comparing multiple persona responses
|
| 120 |
+
|
| 121 |
+
Args:
|
| 122 |
+
personas: List of personas to compare
|
| 123 |
+
question: Question to ask
|
| 124 |
+
|
| 125 |
+
Returns:
|
| 126 |
+
System prompt for comparison
|
| 127 |
+
"""
|
| 128 |
+
persona_summaries = []
|
| 129 |
+
for i, persona in enumerate(personas, 1):
|
| 130 |
+
persona_summaries.append(
|
| 131 |
+
f"PERSONA {i}: {persona.name} ({persona.role})\n"
|
| 132 |
+
f"{persona.tagline}"
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
prompt = f"""You are analyzing responses to an urban planning question from multiple stakeholder perspectives.
|
| 136 |
+
|
| 137 |
+
{chr(10).join(persona_summaries)}
|
| 138 |
+
|
| 139 |
+
Question: {question}
|
| 140 |
+
|
| 141 |
+
For each persona, provide:
|
| 142 |
+
1. Their likely position/response
|
| 143 |
+
2. Key concerns they would raise
|
| 144 |
+
3. Rationale based on their values and background
|
| 145 |
+
|
| 146 |
+
Be concise but capture the distinct perspective of each persona."""
|
| 147 |
+
|
| 148 |
+
return prompt
|
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Persona management and data models"""
|
| 2 |
+
|
| 3 |
+
from .models import Persona, Demographics, Psychographics, BehavioralProfile
|
| 4 |
+
from .database import PersonaDatabase
|
| 5 |
+
|
| 6 |
+
__all__ = [
|
| 7 |
+
"Persona",
|
| 8 |
+
"Demographics",
|
| 9 |
+
"Psychographics",
|
| 10 |
+
"BehavioralProfile",
|
| 11 |
+
"PersonaDatabase",
|
| 12 |
+
]
|
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Persona database and management system"""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Dict, List, Optional
|
| 6 |
+
from .models import Persona
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class PersonaDatabase:
|
| 10 |
+
"""Database for managing and retrieving personas"""
|
| 11 |
+
|
| 12 |
+
def __init__(self, personas_dir: Optional[Path] = None):
|
| 13 |
+
"""
|
| 14 |
+
Initialize persona database
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
personas_dir: Directory containing persona JSON files.
|
| 18 |
+
Defaults to data/personas relative to project root.
|
| 19 |
+
"""
|
| 20 |
+
if personas_dir is None:
|
| 21 |
+
# Default to data/personas from project root
|
| 22 |
+
project_root = Path(__file__).parent.parent.parent
|
| 23 |
+
personas_dir = project_root / "data" / "personas"
|
| 24 |
+
|
| 25 |
+
self.personas_dir = Path(personas_dir)
|
| 26 |
+
self.personas: Dict[str, Persona] = {}
|
| 27 |
+
self._load_personas()
|
| 28 |
+
|
| 29 |
+
def _load_personas(self) -> None:
|
| 30 |
+
"""Load all persona JSON files from the personas directory"""
|
| 31 |
+
if not self.personas_dir.exists():
|
| 32 |
+
raise FileNotFoundError(
|
| 33 |
+
f"Personas directory not found: {self.personas_dir}"
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
json_files = list(self.personas_dir.glob("*.json"))
|
| 37 |
+
if not json_files:
|
| 38 |
+
raise ValueError(
|
| 39 |
+
f"No persona JSON files found in {self.personas_dir}"
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
for json_file in json_files:
|
| 43 |
+
try:
|
| 44 |
+
with open(json_file, "r", encoding="utf-8") as f:
|
| 45 |
+
data = json.load(f)
|
| 46 |
+
persona = Persona(**data)
|
| 47 |
+
self.personas[persona.persona_id] = persona
|
| 48 |
+
except Exception as e:
|
| 49 |
+
print(f"Warning: Failed to load {json_file}: {e}")
|
| 50 |
+
|
| 51 |
+
print(f"Loaded {len(self.personas)} personas")
|
| 52 |
+
|
| 53 |
+
def get_persona(self, persona_id: str) -> Optional[Persona]:
|
| 54 |
+
"""
|
| 55 |
+
Retrieve a persona by ID
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
persona_id: Unique persona identifier
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
Persona object if found, None otherwise
|
| 62 |
+
"""
|
| 63 |
+
return self.personas.get(persona_id)
|
| 64 |
+
|
| 65 |
+
def get_all_personas(self) -> List[Persona]:
|
| 66 |
+
"""
|
| 67 |
+
Get all loaded personas
|
| 68 |
+
|
| 69 |
+
Returns:
|
| 70 |
+
List of all Persona objects
|
| 71 |
+
"""
|
| 72 |
+
return list(self.personas.values())
|
| 73 |
+
|
| 74 |
+
def get_personas_by_role(self, role: str) -> List[Persona]:
|
| 75 |
+
"""
|
| 76 |
+
Get personas matching a specific role
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
role: Role to filter by (case-insensitive partial match)
|
| 80 |
+
|
| 81 |
+
Returns:
|
| 82 |
+
List of matching Persona objects
|
| 83 |
+
"""
|
| 84 |
+
role_lower = role.lower()
|
| 85 |
+
return [
|
| 86 |
+
p for p in self.personas.values()
|
| 87 |
+
if role_lower in p.role.lower()
|
| 88 |
+
]
|
| 89 |
+
|
| 90 |
+
def get_personas_by_criteria(
|
| 91 |
+
self,
|
| 92 |
+
min_age: Optional[int] = None,
|
| 93 |
+
max_age: Optional[int] = None,
|
| 94 |
+
education: Optional[str] = None,
|
| 95 |
+
political_leaning: Optional[str] = None,
|
| 96 |
+
min_environmental_concern: Optional[int] = None,
|
| 97 |
+
) -> List[Persona]:
|
| 98 |
+
"""
|
| 99 |
+
Get personas matching specific criteria
|
| 100 |
+
|
| 101 |
+
Args:
|
| 102 |
+
min_age: Minimum age filter
|
| 103 |
+
max_age: Maximum age filter
|
| 104 |
+
education: Education level filter
|
| 105 |
+
political_leaning: Political leaning filter
|
| 106 |
+
min_environmental_concern: Minimum environmental concern score
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
List of matching Persona objects
|
| 110 |
+
"""
|
| 111 |
+
results = list(self.personas.values())
|
| 112 |
+
|
| 113 |
+
if min_age is not None:
|
| 114 |
+
results = [p for p in results if p.demographics.age >= min_age]
|
| 115 |
+
|
| 116 |
+
if max_age is not None:
|
| 117 |
+
results = [p for p in results if p.demographics.age <= max_age]
|
| 118 |
+
|
| 119 |
+
if education is not None:
|
| 120 |
+
results = [
|
| 121 |
+
p for p in results
|
| 122 |
+
if p.demographics.education == education
|
| 123 |
+
]
|
| 124 |
+
|
| 125 |
+
if political_leaning is not None:
|
| 126 |
+
results = [
|
| 127 |
+
p for p in results
|
| 128 |
+
if p.psychographics.political_leaning == political_leaning
|
| 129 |
+
]
|
| 130 |
+
|
| 131 |
+
if min_environmental_concern is not None:
|
| 132 |
+
results = [
|
| 133 |
+
p for p in results
|
| 134 |
+
if p.psychographics.environmental_concern >= min_environmental_concern
|
| 135 |
+
]
|
| 136 |
+
|
| 137 |
+
return results
|
| 138 |
+
|
| 139 |
+
def list_persona_ids(self) -> List[str]:
|
| 140 |
+
"""
|
| 141 |
+
Get list of all persona IDs
|
| 142 |
+
|
| 143 |
+
Returns:
|
| 144 |
+
List of persona ID strings
|
| 145 |
+
"""
|
| 146 |
+
return list(self.personas.keys())
|
| 147 |
+
|
| 148 |
+
def get_persona_summary(self, persona_id: str) -> Optional[str]:
|
| 149 |
+
"""
|
| 150 |
+
Get a brief summary of a persona
|
| 151 |
+
|
| 152 |
+
Args:
|
| 153 |
+
persona_id: Unique persona identifier
|
| 154 |
+
|
| 155 |
+
Returns:
|
| 156 |
+
Brief text summary or None if persona not found
|
| 157 |
+
"""
|
| 158 |
+
persona = self.get_persona(persona_id)
|
| 159 |
+
if persona is None:
|
| 160 |
+
return None
|
| 161 |
+
|
| 162 |
+
return f"{persona.name} - {persona.role}: {persona.tagline}"
|
| 163 |
+
|
| 164 |
+
def search_personas(self, keyword: str) -> List[Persona]:
|
| 165 |
+
"""
|
| 166 |
+
Search personas by keyword in various fields
|
| 167 |
+
|
| 168 |
+
Args:
|
| 169 |
+
keyword: Search term (case-insensitive)
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
List of matching Persona objects
|
| 173 |
+
"""
|
| 174 |
+
keyword_lower = keyword.lower()
|
| 175 |
+
results = []
|
| 176 |
+
|
| 177 |
+
for persona in self.personas.values():
|
| 178 |
+
# Search in various text fields
|
| 179 |
+
searchable_text = " ".join([
|
| 180 |
+
persona.name,
|
| 181 |
+
persona.role,
|
| 182 |
+
persona.tagline,
|
| 183 |
+
persona.background_story,
|
| 184 |
+
" ".join(persona.psychographics.core_values),
|
| 185 |
+
" ".join(persona.psychographics.priorities),
|
| 186 |
+
]).lower()
|
| 187 |
+
|
| 188 |
+
if keyword_lower in searchable_text:
|
| 189 |
+
results.append(persona)
|
| 190 |
+
|
| 191 |
+
return results
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def load_persona_from_file(filepath: Path) -> Persona:
|
| 195 |
+
"""
|
| 196 |
+
Load a single persona from a JSON file
|
| 197 |
+
|
| 198 |
+
Args:
|
| 199 |
+
filepath: Path to JSON file
|
| 200 |
+
|
| 201 |
+
Returns:
|
| 202 |
+
Persona object
|
| 203 |
+
"""
|
| 204 |
+
with open(filepath, "r", encoding="utf-8") as f:
|
| 205 |
+
data = json.load(f)
|
| 206 |
+
return Persona(**data)
|
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Persona data models for urban planning stakeholders"""
|
| 2 |
+
|
| 3 |
+
from typing import List, Dict, Optional, Any
|
| 4 |
+
from pydantic import BaseModel, Field
|
| 5 |
+
from enum import Enum
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class EducationLevel(str, Enum):
|
| 9 |
+
"""Education level enumeration"""
|
| 10 |
+
HIGH_SCHOOL = "high_school"
|
| 11 |
+
SOME_COLLEGE = "some_college"
|
| 12 |
+
BACHELORS = "bachelors"
|
| 13 |
+
MASTERS = "masters"
|
| 14 |
+
DOCTORATE = "doctorate"
|
| 15 |
+
PROFESSIONAL = "professional"
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class IncomeLevel(str, Enum):
|
| 19 |
+
"""Income level categories"""
|
| 20 |
+
LOW = "low" # <$35k
|
| 21 |
+
LOWER_MIDDLE = "lower_middle" # $35k-$50k
|
| 22 |
+
MIDDLE = "middle" # $50k-$75k
|
| 23 |
+
UPPER_MIDDLE = "upper_middle" # $75k-$150k
|
| 24 |
+
HIGH = "high" # $150k-$300k
|
| 25 |
+
VERY_HIGH = "very_high" # >$300k
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class PoliticalLeaning(str, Enum):
|
| 29 |
+
"""Political orientation spectrum"""
|
| 30 |
+
VERY_PROGRESSIVE = "very_progressive"
|
| 31 |
+
PROGRESSIVE = "progressive"
|
| 32 |
+
MODERATE = "moderate"
|
| 33 |
+
CONSERVATIVE = "conservative"
|
| 34 |
+
VERY_CONSERVATIVE = "very_conservative"
|
| 35 |
+
INDEPENDENT = "independent"
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class Demographics(BaseModel):
|
| 39 |
+
"""Demographic information for a persona"""
|
| 40 |
+
age: int = Field(..., ge=18, le=100, description="Age in years")
|
| 41 |
+
gender: str = Field(..., description="Gender identity")
|
| 42 |
+
education: EducationLevel = Field(..., description="Highest education level")
|
| 43 |
+
occupation: str = Field(..., description="Current occupation")
|
| 44 |
+
income_level: IncomeLevel = Field(..., description="Income bracket")
|
| 45 |
+
location_type: str = Field(..., description="Urban/suburban/rural residence")
|
| 46 |
+
years_in_community: int = Field(..., ge=0, description="Years living in community")
|
| 47 |
+
household_size: int = Field(default=1, ge=1, description="Number in household")
|
| 48 |
+
has_children: bool = Field(default=False, description="Has children")
|
| 49 |
+
owns_home: bool = Field(default=False, description="Homeowner status")
|
| 50 |
+
commute_method: str = Field(default="car", description="Primary commute method")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class Psychographics(BaseModel):
|
| 54 |
+
"""Psychological characteristics and values"""
|
| 55 |
+
core_values: List[str] = Field(
|
| 56 |
+
default_factory=list,
|
| 57 |
+
description="Core values (e.g., sustainability, tradition, innovation)"
|
| 58 |
+
)
|
| 59 |
+
priorities: List[str] = Field(
|
| 60 |
+
default_factory=list,
|
| 61 |
+
description="Top priorities in urban planning"
|
| 62 |
+
)
|
| 63 |
+
political_leaning: PoliticalLeaning = Field(
|
| 64 |
+
..., description="Political orientation"
|
| 65 |
+
)
|
| 66 |
+
openness_to_change: int = Field(
|
| 67 |
+
..., ge=1, le=10,
|
| 68 |
+
description="Openness to change (1=resistant, 10=embraces)"
|
| 69 |
+
)
|
| 70 |
+
community_engagement: int = Field(
|
| 71 |
+
..., ge=1, le=10,
|
| 72 |
+
description="Community involvement level"
|
| 73 |
+
)
|
| 74 |
+
environmental_concern: int = Field(
|
| 75 |
+
..., ge=1, le=10,
|
| 76 |
+
description="Environmental consciousness"
|
| 77 |
+
)
|
| 78 |
+
economic_focus: int = Field(
|
| 79 |
+
..., ge=1, le=10,
|
| 80 |
+
description="Prioritization of economic factors"
|
| 81 |
+
)
|
| 82 |
+
social_equity_focus: int = Field(
|
| 83 |
+
..., ge=1, le=10,
|
| 84 |
+
description="Focus on social equity and justice"
|
| 85 |
+
)
|
| 86 |
+
risk_tolerance: int = Field(
|
| 87 |
+
..., ge=1, le=10,
|
| 88 |
+
description="Willingness to accept risk/uncertainty"
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class BehavioralProfile(BaseModel):
|
| 93 |
+
"""Behavioral patterns and communication style"""
|
| 94 |
+
communication_style: str = Field(
|
| 95 |
+
...,
|
| 96 |
+
description="Communication approach (e.g., analytical, emotional, diplomatic)"
|
| 97 |
+
)
|
| 98 |
+
decision_making_approach: str = Field(
|
| 99 |
+
...,
|
| 100 |
+
description="How they make decisions (data-driven, intuitive, consultative)"
|
| 101 |
+
)
|
| 102 |
+
conflict_resolution_style: str = Field(
|
| 103 |
+
...,
|
| 104 |
+
description="Approach to conflict (collaborative, assertive, accommodating)"
|
| 105 |
+
)
|
| 106 |
+
typical_concerns: List[str] = Field(
|
| 107 |
+
default_factory=list,
|
| 108 |
+
description="Common concerns they raise"
|
| 109 |
+
)
|
| 110 |
+
typical_language_patterns: List[str] = Field(
|
| 111 |
+
default_factory=list,
|
| 112 |
+
description="Characteristic phrases or language patterns"
|
| 113 |
+
)
|
| 114 |
+
engagement_preferences: List[str] = Field(
|
| 115 |
+
default_factory=list,
|
| 116 |
+
description="Preferred ways to engage (meetings, online, informal)"
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class KnowledgeDomain(BaseModel):
|
| 121 |
+
"""Areas of knowledge and expertise"""
|
| 122 |
+
domain: str = Field(..., description="Knowledge domain name")
|
| 123 |
+
expertise_level: int = Field(..., ge=1, le=10, description="Expertise level")
|
| 124 |
+
experience_years: int = Field(default=0, ge=0, description="Years of experience")
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
class Persona(BaseModel):
|
| 128 |
+
"""Complete persona model for urban planning stakeholder"""
|
| 129 |
+
|
| 130 |
+
# Identity
|
| 131 |
+
persona_id: str = Field(..., description="Unique identifier")
|
| 132 |
+
name: str = Field(..., description="Full name")
|
| 133 |
+
role: str = Field(..., description="Primary role in urban planning context")
|
| 134 |
+
tagline: str = Field(..., description="Brief description of persona")
|
| 135 |
+
|
| 136 |
+
# Core attributes
|
| 137 |
+
demographics: Demographics
|
| 138 |
+
psychographics: Psychographics
|
| 139 |
+
behavioral_profile: BehavioralProfile
|
| 140 |
+
|
| 141 |
+
# Knowledge and expertise
|
| 142 |
+
knowledge_domains: List[KnowledgeDomain] = Field(default_factory=list)
|
| 143 |
+
|
| 144 |
+
# Context
|
| 145 |
+
background_story: str = Field(
|
| 146 |
+
...,
|
| 147 |
+
description="Narrative background providing context for views"
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
# Stakeholder relationships
|
| 151 |
+
affiliated_organizations: List[str] = Field(
|
| 152 |
+
default_factory=list,
|
| 153 |
+
description="Organizations/groups they're associated with"
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
# Additional metadata
|
| 157 |
+
metadata: Dict[str, Any] = Field(
|
| 158 |
+
default_factory=dict,
|
| 159 |
+
description="Additional flexible metadata"
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
class Config:
|
| 163 |
+
"""Pydantic config"""
|
| 164 |
+
use_enum_values = True
|
| 165 |
+
json_schema_extra = {
|
| 166 |
+
"example": {
|
| 167 |
+
"persona_id": "sarah_chen",
|
| 168 |
+
"name": "Sarah Chen",
|
| 169 |
+
"role": "Urban Planner",
|
| 170 |
+
"tagline": "Progressive city planner focused on sustainability",
|
| 171 |
+
"demographics": {
|
| 172 |
+
"age": 34,
|
| 173 |
+
"gender": "female",
|
| 174 |
+
"education": "masters",
|
| 175 |
+
"occupation": "Urban Planner",
|
| 176 |
+
"income_level": "upper_middle",
|
| 177 |
+
"location_type": "urban",
|
| 178 |
+
"years_in_community": 8,
|
| 179 |
+
},
|
| 180 |
+
"psychographics": {
|
| 181 |
+
"core_values": ["sustainability", "innovation", "equity"],
|
| 182 |
+
"political_leaning": "progressive",
|
| 183 |
+
"openness_to_change": 9,
|
| 184 |
+
},
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
def get_context_summary(self) -> str:
|
| 189 |
+
"""Generate a text summary of persona for LLM context"""
|
| 190 |
+
return f"""
|
| 191 |
+
Persona: {self.name} ({self.role})
|
| 192 |
+
Background: {self.tagline}
|
| 193 |
+
|
| 194 |
+
Demographics:
|
| 195 |
+
- Age: {self.demographics.age}, {self.demographics.gender}
|
| 196 |
+
- Education: {self.demographics.education}
|
| 197 |
+
- Occupation: {self.demographics.occupation}
|
| 198 |
+
- Years in community: {self.demographics.years_in_community}
|
| 199 |
+
- Commute: {self.demographics.commute_method}
|
| 200 |
+
|
| 201 |
+
Values & Priorities:
|
| 202 |
+
- Core values: {', '.join(self.psychographics.core_values)}
|
| 203 |
+
- Political leaning: {self.psychographics.political_leaning}
|
| 204 |
+
- Openness to change: {self.psychographics.openness_to_change}/10
|
| 205 |
+
- Environmental concern: {self.psychographics.environmental_concern}/10
|
| 206 |
+
- Economic focus: {self.psychographics.economic_focus}/10
|
| 207 |
+
- Social equity focus: {self.psychographics.social_equity_focus}/10
|
| 208 |
+
|
| 209 |
+
Communication Style: {self.behavioral_profile.communication_style}
|
| 210 |
+
Decision Making: {self.behavioral_profile.decision_making_approach}
|
| 211 |
+
|
| 212 |
+
Background: {self.background_story}
|
| 213 |
+
""".strip()
|
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Query-response pipeline for persona interactions"""
|
| 2 |
+
|
| 3 |
+
from .query_engine import QueryEngine, QueryResponse
|
| 4 |
+
|
| 5 |
+
__all__ = ["QueryEngine", "QueryResponse"]
|
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Main query engine for persona-based responses"""
|
| 2 |
+
|
| 3 |
+
from typing import Optional, Dict, Any
|
| 4 |
+
from pydantic import BaseModel
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
from ..personas.database import PersonaDatabase
|
| 8 |
+
from ..context.database import ContextDatabase
|
| 9 |
+
from ..llm.anthropic_client import AnthropicClient
|
| 10 |
+
from ..llm.prompt_builder import PromptBuilder
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class QueryResponse(BaseModel):
|
| 14 |
+
"""Structured response from a persona query"""
|
| 15 |
+
|
| 16 |
+
persona_id: str
|
| 17 |
+
persona_name: str
|
| 18 |
+
persona_role: str
|
| 19 |
+
question: str
|
| 20 |
+
response: str
|
| 21 |
+
context_id: Optional[str] = None
|
| 22 |
+
timestamp: str
|
| 23 |
+
model_used: str
|
| 24 |
+
metadata: Dict[str, Any] = {}
|
| 25 |
+
|
| 26 |
+
class Config:
|
| 27 |
+
"""Pydantic config"""
|
| 28 |
+
json_schema_extra = {
|
| 29 |
+
"example": {
|
| 30 |
+
"persona_id": "sarah_chen",
|
| 31 |
+
"persona_name": "Sarah Chen",
|
| 32 |
+
"persona_role": "Urban Planner",
|
| 33 |
+
"question": "What do you think about the bike lane proposal?",
|
| 34 |
+
"response": "I strongly support this bike lane proposal...",
|
| 35 |
+
"context_id": "downtown_district",
|
| 36 |
+
"timestamp": "2024-03-15T10:30:00",
|
| 37 |
+
"model_used": "claude-3-5-sonnet-20241022",
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class QueryEngine:
|
| 43 |
+
"""Main engine for querying personas and generating responses"""
|
| 44 |
+
|
| 45 |
+
def __init__(
|
| 46 |
+
self,
|
| 47 |
+
persona_db: Optional[PersonaDatabase] = None,
|
| 48 |
+
context_db: Optional[ContextDatabase] = None,
|
| 49 |
+
llm_client: Optional[AnthropicClient] = None,
|
| 50 |
+
):
|
| 51 |
+
"""
|
| 52 |
+
Initialize query engine
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
persona_db: Persona database instance (creates default if None)
|
| 56 |
+
context_db: Context database instance (creates default if None)
|
| 57 |
+
llm_client: LLM client instance (creates default if None)
|
| 58 |
+
"""
|
| 59 |
+
self.persona_db = persona_db or PersonaDatabase()
|
| 60 |
+
self.context_db = context_db or ContextDatabase()
|
| 61 |
+
self.llm_client = llm_client or AnthropicClient()
|
| 62 |
+
self.prompt_builder = PromptBuilder()
|
| 63 |
+
|
| 64 |
+
def query(
|
| 65 |
+
self,
|
| 66 |
+
persona_id: str,
|
| 67 |
+
question: str,
|
| 68 |
+
context_id: Optional[str] = None,
|
| 69 |
+
scenario_description: Optional[str] = None,
|
| 70 |
+
temperature: Optional[float] = None,
|
| 71 |
+
max_tokens: Optional[int] = None,
|
| 72 |
+
) -> QueryResponse:
|
| 73 |
+
"""
|
| 74 |
+
Query a persona with a question
|
| 75 |
+
|
| 76 |
+
Args:
|
| 77 |
+
persona_id: ID of persona to query
|
| 78 |
+
question: Question to ask the persona
|
| 79 |
+
context_id: Optional environmental context ID
|
| 80 |
+
scenario_description: Optional scenario description
|
| 81 |
+
temperature: Optional temperature override
|
| 82 |
+
max_tokens: Optional max_tokens override
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
QueryResponse object with the persona's response
|
| 86 |
+
|
| 87 |
+
Raises:
|
| 88 |
+
ValueError: If persona not found
|
| 89 |
+
"""
|
| 90 |
+
# Get persona
|
| 91 |
+
persona = self.persona_db.get_persona(persona_id)
|
| 92 |
+
if persona is None:
|
| 93 |
+
available = ", ".join(self.persona_db.list_persona_ids())
|
| 94 |
+
raise ValueError(
|
| 95 |
+
f"Persona '{persona_id}' not found. "
|
| 96 |
+
f"Available personas: {available}"
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
# Get context if specified
|
| 100 |
+
context = None
|
| 101 |
+
if context_id:
|
| 102 |
+
context = self.context_db.get_context(context_id)
|
| 103 |
+
if context is None:
|
| 104 |
+
print(f"Warning: Context '{context_id}' not found, proceeding without context")
|
| 105 |
+
|
| 106 |
+
# Build prompts
|
| 107 |
+
system_prompt = self.prompt_builder.build_persona_system_prompt(
|
| 108 |
+
persona=persona,
|
| 109 |
+
context=context,
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
user_message = self.prompt_builder.build_contextual_query(
|
| 113 |
+
question=question,
|
| 114 |
+
scenario_description=scenario_description,
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
# Generate response
|
| 118 |
+
response_text = self.llm_client.generate_response(
|
| 119 |
+
system_prompt=system_prompt,
|
| 120 |
+
user_message=user_message,
|
| 121 |
+
temperature=temperature,
|
| 122 |
+
max_tokens=max_tokens,
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
# Build response object
|
| 126 |
+
return QueryResponse(
|
| 127 |
+
persona_id=persona.persona_id,
|
| 128 |
+
persona_name=persona.name,
|
| 129 |
+
persona_role=persona.role,
|
| 130 |
+
question=question,
|
| 131 |
+
response=response_text,
|
| 132 |
+
context_id=context_id,
|
| 133 |
+
timestamp=datetime.now().isoformat(),
|
| 134 |
+
model_used=self.llm_client.model,
|
| 135 |
+
metadata={
|
| 136 |
+
"scenario_description": scenario_description,
|
| 137 |
+
"temperature": temperature or self.llm_client.temperature,
|
| 138 |
+
"max_tokens": max_tokens or self.llm_client.max_tokens,
|
| 139 |
+
},
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
def query_multiple(
|
| 143 |
+
self,
|
| 144 |
+
persona_ids: list[str],
|
| 145 |
+
question: str,
|
| 146 |
+
context_id: Optional[str] = None,
|
| 147 |
+
scenario_description: Optional[str] = None,
|
| 148 |
+
) -> list[QueryResponse]:
|
| 149 |
+
"""
|
| 150 |
+
Query multiple personas with the same question
|
| 151 |
+
|
| 152 |
+
Args:
|
| 153 |
+
persona_ids: List of persona IDs to query
|
| 154 |
+
question: Question to ask all personas
|
| 155 |
+
context_id: Optional environmental context ID
|
| 156 |
+
scenario_description: Optional scenario description
|
| 157 |
+
|
| 158 |
+
Returns:
|
| 159 |
+
List of QueryResponse objects
|
| 160 |
+
"""
|
| 161 |
+
responses = []
|
| 162 |
+
for persona_id in persona_ids:
|
| 163 |
+
try:
|
| 164 |
+
response = self.query(
|
| 165 |
+
persona_id=persona_id,
|
| 166 |
+
question=question,
|
| 167 |
+
context_id=context_id,
|
| 168 |
+
scenario_description=scenario_description,
|
| 169 |
+
)
|
| 170 |
+
responses.append(response)
|
| 171 |
+
except Exception as e:
|
| 172 |
+
print(f"Error querying persona {persona_id}: {e}")
|
| 173 |
+
|
| 174 |
+
return responses
|
| 175 |
+
|
| 176 |
+
def list_available_personas(self) -> list[tuple[str, str, str]]:
|
| 177 |
+
"""
|
| 178 |
+
List all available personas
|
| 179 |
+
|
| 180 |
+
Returns:
|
| 181 |
+
List of (persona_id, name, role) tuples
|
| 182 |
+
"""
|
| 183 |
+
personas = self.persona_db.get_all_personas()
|
| 184 |
+
return [
|
| 185 |
+
(p.persona_id, p.name, p.role)
|
| 186 |
+
for p in personas
|
| 187 |
+
]
|
| 188 |
+
|
| 189 |
+
def list_available_contexts(self) -> list[str]:
|
| 190 |
+
"""
|
| 191 |
+
List all available contexts
|
| 192 |
+
|
| 193 |
+
Returns:
|
| 194 |
+
List of context IDs
|
| 195 |
+
"""
|
| 196 |
+
return self.context_db.list_context_ids()
|
| 197 |
+
|
| 198 |
+
def test_system(self) -> bool:
|
| 199 |
+
"""
|
| 200 |
+
Test that all system components are working
|
| 201 |
+
|
| 202 |
+
Returns:
|
| 203 |
+
True if system is operational
|
| 204 |
+
"""
|
| 205 |
+
try:
|
| 206 |
+
# Check personas loaded
|
| 207 |
+
personas = self.persona_db.get_all_personas()
|
| 208 |
+
if not personas:
|
| 209 |
+
print("Error: No personas loaded")
|
| 210 |
+
return False
|
| 211 |
+
print(f"β Loaded {len(personas)} personas")
|
| 212 |
+
|
| 213 |
+
# Check contexts loaded (optional)
|
| 214 |
+
contexts = self.context_db.get_all_contexts()
|
| 215 |
+
print(f"β Loaded {len(contexts)} contexts")
|
| 216 |
+
|
| 217 |
+
# Check LLM connection
|
| 218 |
+
if self.llm_client.test_connection():
|
| 219 |
+
print(f"β LLM client connected ({self.llm_client.model})")
|
| 220 |
+
else:
|
| 221 |
+
print("Error: LLM client connection failed")
|
| 222 |
+
return False
|
| 223 |
+
|
| 224 |
+
return True
|
| 225 |
+
|
| 226 |
+
except Exception as e:
|
| 227 |
+
print(f"System test failed: {e}")
|
| 228 |
+
return False
|
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Phase 2: Population-based response distributions
|
| 3 |
+
|
| 4 |
+
This module will enable:
|
| 5 |
+
- Generating variants of personas with statistical distributions
|
| 6 |
+
- Querying populations of persona variants
|
| 7 |
+
- Analyzing and visualizing response distributions
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
# Phase 2 implementation coming soon
|
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Basic functionality tests for AI Personas system
|
| 3 |
+
|
| 4 |
+
Run with: pytest tests/test_basic_functionality.py
|
| 5 |
+
or: python tests/test_basic_functionality.py
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
# Add src to path
|
| 12 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 13 |
+
|
| 14 |
+
from src.personas.database import PersonaDatabase
|
| 15 |
+
from src.context.database import ContextDatabase
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def test_personas_load():
|
| 19 |
+
"""Test that personas load correctly"""
|
| 20 |
+
db = PersonaDatabase()
|
| 21 |
+
personas = db.get_all_personas()
|
| 22 |
+
|
| 23 |
+
assert len(personas) == 6, f"Expected 6 personas, got {len(personas)}"
|
| 24 |
+
|
| 25 |
+
# Check specific personas exist
|
| 26 |
+
expected_ids = [
|
| 27 |
+
"sarah_chen",
|
| 28 |
+
"marcus_thompson",
|
| 29 |
+
"elena_rodriguez",
|
| 30 |
+
"james_obrien",
|
| 31 |
+
"priya_patel",
|
| 32 |
+
"david_kim",
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
for persona_id in expected_ids:
|
| 36 |
+
persona = db.get_persona(persona_id)
|
| 37 |
+
assert persona is not None, f"Persona {persona_id} not found"
|
| 38 |
+
assert persona.name, f"Persona {persona_id} has no name"
|
| 39 |
+
assert persona.role, f"Persona {persona_id} has no role"
|
| 40 |
+
|
| 41 |
+
print("β All 6 personas loaded successfully")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def test_persona_attributes():
|
| 45 |
+
"""Test that personas have required attributes"""
|
| 46 |
+
db = PersonaDatabase()
|
| 47 |
+
sarah = db.get_persona("sarah_chen")
|
| 48 |
+
|
| 49 |
+
assert sarah is not None
|
| 50 |
+
assert sarah.persona_id == "sarah_chen"
|
| 51 |
+
assert sarah.name == "Sarah Chen"
|
| 52 |
+
assert sarah.demographics.age > 0
|
| 53 |
+
assert len(sarah.psychographics.core_values) > 0
|
| 54 |
+
assert len(sarah.behavioral_profile.typical_concerns) > 0
|
| 55 |
+
assert sarah.background_story
|
| 56 |
+
|
| 57 |
+
print("β Persona attributes validated")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def test_contexts_load():
|
| 61 |
+
"""Test that contexts load correctly"""
|
| 62 |
+
db = ContextDatabase()
|
| 63 |
+
contexts = db.get_all_contexts()
|
| 64 |
+
|
| 65 |
+
# At least one context should exist
|
| 66 |
+
assert len(contexts) >= 1, "No contexts found"
|
| 67 |
+
|
| 68 |
+
# Check downtown_district exists
|
| 69 |
+
downtown = db.get_context("downtown_district")
|
| 70 |
+
assert downtown is not None, "downtown_district context not found"
|
| 71 |
+
assert downtown.built_environment.name
|
| 72 |
+
assert downtown.social_context.median_income > 0
|
| 73 |
+
|
| 74 |
+
print(f"β {len(contexts)} context(s) loaded successfully")
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def test_persona_search():
|
| 78 |
+
"""Test persona search functionality"""
|
| 79 |
+
db = PersonaDatabase()
|
| 80 |
+
|
| 81 |
+
# Search by keyword
|
| 82 |
+
results = db.search_personas("planner")
|
| 83 |
+
assert len(results) > 0, "Search for 'planner' found no results"
|
| 84 |
+
|
| 85 |
+
# Search by role
|
| 86 |
+
results = db.get_personas_by_role("developer")
|
| 87 |
+
assert len(results) > 0, "No developers found"
|
| 88 |
+
|
| 89 |
+
print("β Persona search working")
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def test_persona_filtering():
|
| 93 |
+
"""Test persona filtering by criteria"""
|
| 94 |
+
db = PersonaDatabase()
|
| 95 |
+
|
| 96 |
+
# Filter by age
|
| 97 |
+
young = db.get_personas_by_criteria(max_age=35)
|
| 98 |
+
assert len(young) > 0, "No young personas found"
|
| 99 |
+
|
| 100 |
+
# Filter by environmental concern
|
| 101 |
+
environmentalists = db.get_personas_by_criteria(min_environmental_concern=8)
|
| 102 |
+
assert len(environmentalists) > 0, "No environmentally concerned personas found"
|
| 103 |
+
|
| 104 |
+
print("β Persona filtering working")
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def test_context_summary():
|
| 108 |
+
"""Test that context summaries generate"""
|
| 109 |
+
db = ContextDatabase()
|
| 110 |
+
context = db.get_default_context()
|
| 111 |
+
|
| 112 |
+
if context:
|
| 113 |
+
summary = context.get_context_summary()
|
| 114 |
+
assert len(summary) > 100, "Context summary too short"
|
| 115 |
+
assert "LOCATION" in summary
|
| 116 |
+
print("β Context summary generation working")
|
| 117 |
+
else:
|
| 118 |
+
print("β No contexts available to test summary")
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def test_persona_summary():
|
| 122 |
+
"""Test that persona summaries generate"""
|
| 123 |
+
db = PersonaDatabase()
|
| 124 |
+
sarah = db.get_persona("sarah_chen")
|
| 125 |
+
|
| 126 |
+
summary = sarah.get_context_summary()
|
| 127 |
+
assert len(summary) > 100, "Persona summary too short"
|
| 128 |
+
assert "Sarah Chen" in summary
|
| 129 |
+
assert "Urban Planner" in summary
|
| 130 |
+
|
| 131 |
+
print("β Persona summary generation working")
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def run_all_tests():
|
| 135 |
+
"""Run all tests"""
|
| 136 |
+
print("\n" + "=" * 70)
|
| 137 |
+
print("Running Basic Functionality Tests")
|
| 138 |
+
print("=" * 70 + "\n")
|
| 139 |
+
|
| 140 |
+
tests = [
|
| 141 |
+
test_personas_load,
|
| 142 |
+
test_persona_attributes,
|
| 143 |
+
test_contexts_load,
|
| 144 |
+
test_persona_search,
|
| 145 |
+
test_persona_filtering,
|
| 146 |
+
test_context_summary,
|
| 147 |
+
test_persona_summary,
|
| 148 |
+
]
|
| 149 |
+
|
| 150 |
+
failed = 0
|
| 151 |
+
for test in tests:
|
| 152 |
+
try:
|
| 153 |
+
test()
|
| 154 |
+
except AssertionError as e:
|
| 155 |
+
print(f"β {test.__name__} FAILED: {e}")
|
| 156 |
+
failed += 1
|
| 157 |
+
except Exception as e:
|
| 158 |
+
print(f"β {test.__name__} ERROR: {e}")
|
| 159 |
+
failed += 1
|
| 160 |
+
|
| 161 |
+
print("\n" + "=" * 70)
|
| 162 |
+
if failed == 0:
|
| 163 |
+
print("β All tests passed!")
|
| 164 |
+
else:
|
| 165 |
+
print(f"β {failed} test(s) failed")
|
| 166 |
+
print("=" * 70 + "\n")
|
| 167 |
+
|
| 168 |
+
return failed == 0
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
if __name__ == "__main__":
|
| 172 |
+
success = run_all_tests()
|
| 173 |
+
sys.exit(0 if success else 1)
|