Spaces:
Paused
Paused
feat: introduce Council Discussion stage in DPR AI Simulator, enabling multi-member deliberation with consensus building. Update README to reflect new features and enhance simulation logic to include council discussions, along with necessary adjustments in agent and response models.
5c072a2 | """Main DPR AI Simulator class orchestrating the pipeline.""" | |
| import asyncio | |
| from typing import List, Optional, Callable | |
| from datetime import datetime | |
| import logging | |
| from ..config import settings | |
| from ..models import ( | |
| DPRMember, | |
| Aspirasi, | |
| AbsorpsiResponse, | |
| KompilasiResponse, | |
| TindakLanjutResponse, | |
| SimulationDetails, | |
| PipelineResult, | |
| ) | |
| from .member_factory import DPRMemberFactory | |
| from .agents import AbsorbAgent, CompileAgent, FollowUpAgent, CouncilDiscussionAgent | |
| logger = logging.getLogger("dpr_simulator.simulator") | |
| class DPRSimulator: | |
| """ | |
| Main simulator class that orchestrates the DPR aspiration processing pipeline. | |
| Pipeline stages: | |
| 1. Menyerap (Absorb) - AI agents absorb and understand aspirations | |
| 2. Menghimpun (Compile) - Aggregate responses from multiple members | |
| 3. Menindaklanjuti (Follow-up) - Determine concrete follow-up actions | |
| """ | |
| def __init__( | |
| self, | |
| api_key: Optional[str] = None, | |
| model: Optional[str] = None, | |
| ): | |
| """ | |
| Initialize the DPR AI Simulator. | |
| Args: | |
| api_key: OpenAI API key (defaults to settings) | |
| model: OpenAI model name (defaults to settings) | |
| """ | |
| self.api_key = api_key or settings.openai_api_key | |
| self.model = model or settings.openai_model | |
| logger.info(f"Initializing DPR Simulator with model: {self.model}") | |
| # Initialize agents | |
| logger.debug("Initializing agents (Absorb, Compile, CouncilDiscussion, FollowUp)") | |
| self.absorb_agent = AbsorbAgent(api_key=self.api_key, model=self.model) | |
| self.compile_agent = CompileAgent(api_key=self.api_key, model=self.model) | |
| self.council_discussion_agent = CouncilDiscussionAgent(api_key=self.api_key, model=self.model) | |
| self.followup_agent = FollowUpAgent(api_key=self.api_key, model=self.model) | |
| # Initialize members | |
| self.members: List[DPRMember] = [] | |
| self.aspirations: List[Aspirasi] = [] | |
| logger.info("DPR Simulator initialized successfully") | |
| def create_members(self, count: int = None) -> List[DPRMember]: | |
| """ | |
| Create simulated DPR members. | |
| Args: | |
| count: Number of members to create (defaults to settings) | |
| Returns: | |
| List of created DPRMember instances | |
| """ | |
| count = count or settings.default_member_count | |
| logger.info(f"Creating {count} DPR members") | |
| self.members = DPRMemberFactory.create_members(count) | |
| # Log member distribution | |
| factions = {} | |
| komisi = {} | |
| for m in self.members: | |
| factions[m.faction] = factions.get(m.faction, 0) + 1 | |
| komisi[m.komisi] = komisi.get(m.komisi, 0) + 1 | |
| logger.info(f"Member distribution - Factions: {len(factions)}, Commissions: {len(komisi)}") | |
| logger.debug(f"Factions breakdown: {dict(factions)}") | |
| logger.debug(f"Commissions breakdown: {dict(komisi)}") | |
| return self.members | |
| def add_aspirasi(self, aspirasi: Aspirasi) -> None: | |
| """Add a public aspiration to the system.""" | |
| self.aspirations.append(aspirasi) | |
| async def _process_absorb_batch( | |
| self, | |
| members: List[DPRMember], | |
| aspirasi: Aspirasi, | |
| progress_callback: Optional[Callable[[str], None]] = None, | |
| batch_num: int = 0, | |
| total_batches: int = 0, | |
| ) -> List[AbsorpsiResponse]: | |
| """Process a batch of members for the absorb stage.""" | |
| logger.info(f"Processing absorb batch {batch_num}/{total_batches} with {len(members)} members") | |
| tasks = [self.absorb_agent.invoke(member, aspirasi) for member in members] | |
| results = await asyncio.gather(*tasks) | |
| # Log batch results | |
| success_count = sum(1 for r in results if r.error is None) | |
| error_count = len(results) - success_count | |
| batch_cost = sum(r.cost_usd for r in results) | |
| logger.info(f"Batch {batch_num} completed - Success: {success_count}, Errors: {error_count}, Cost: ${batch_cost:.6f}") | |
| if error_count > 0: | |
| errors = [r.error for r in results if r.error] | |
| logger.warning(f"Batch {batch_num} errors: {errors}") | |
| return list(results) | |
| async def process_aspirasi( | |
| self, | |
| aspirasi: Aspirasi, | |
| sample_size: int = None, | |
| komisi_filter: Optional[str] = None, | |
| progress_callback: Optional[Callable[[str], None]] = None, | |
| ) -> PipelineResult: | |
| """ | |
| Process a single aspiration through the complete pipeline. | |
| Args: | |
| aspirasi: The aspiration to process | |
| sample_size: Number of members to sample (defaults to settings) | |
| komisi_filter: Optional specific commission to filter by | |
| progress_callback: Optional callback for progress updates | |
| Returns: | |
| PipelineResult with complete processing results | |
| """ | |
| sample_size = sample_size or settings.default_member_count | |
| batch_size = settings.batch_size | |
| total_cost = 0.0 | |
| logger.info("=" * 60) | |
| logger.info(f"STARTING PIPELINE - Aspirasi ID: {aspirasi.id}") | |
| logger.info(f" Category: {aspirasi.category}") | |
| logger.info(f" Source: {aspirasi.source}") | |
| logger.info(f" Priority: {aspirasi.priority}") | |
| logger.info(f" Sample Size: {sample_size}") | |
| logger.info(f" Komisi Filter: {komisi_filter or 'Auto'}") | |
| logger.info("=" * 60) | |
| def log(msg: str): | |
| if progress_callback: | |
| progress_callback(msg) | |
| log(f"๐ Aspirasi telah diterima, memproses aspirasi sekarang") | |
| # Get relevant members | |
| logger.info("Finding relevant members...") | |
| relevant_members = DPRMemberFactory.get_relevant_members( | |
| self.members, aspirasi.category, aspirasi.source, komisi_filter, sample_size | |
| ) | |
| logger.info(f"Found {len(relevant_members)} relevant members from {len(self.members)} total") | |
| log(f"๐ Ditemukan {len(relevant_members)} anggota relevan") | |
| # Step 1: Menyerap (Absorb) | |
| logger.info(f"[STEP 1: ABSORB] Processing {len(relevant_members)} members in batches of {batch_size}") | |
| log(f"๐ฅ Step 1: Menyerap aspirasi oleh {len(relevant_members)} anggota") | |
| all_responses: List[AbsorpsiResponse] = [] | |
| total_batches = (len(relevant_members) + batch_size - 1) // batch_size | |
| for i in range(0, len(relevant_members), batch_size): | |
| batch_num = (i // batch_size) + 1 | |
| batch = relevant_members[i : i + batch_size] | |
| batch_responses = await self._process_absorb_batch( | |
| batch, aspirasi, progress_callback, batch_num, total_batches | |
| ) | |
| all_responses.extend(batch_responses) | |
| total_cost += sum(r.cost_usd for r in batch_responses) | |
| # Rate limiting | |
| if i + batch_size < len(relevant_members): | |
| logger.debug(f"Rate limiting: sleeping for {settings.rate_limit_delay}s") | |
| await asyncio.sleep(settings.rate_limit_delay) | |
| # Calculate absorb statistics | |
| absorb_cost = sum(r.cost_usd for r in all_responses) | |
| success_responses = [r for r in all_responses if r.error is None] | |
| error_responses = [r for r in all_responses if r.error] | |
| logger.info(f"[STEP 1: ABSORB] Completed - {len(success_responses)} success, {len(error_responses)} errors, Cost: ${absorb_cost:.6f}") | |
| log(f"โ Step 1 selesai: {len(all_responses)} tanggapan dikumpulkan") | |
| # Step 2: Menghimpun (Compile) | |
| logger.info("[STEP 2: COMPILE] Compiling responses...") | |
| log("๐ Step 2: Menghimpun tanggapan anggota") | |
| kompilasi = await self.compile_agent.invoke(aspirasi, all_responses) | |
| total_cost += kompilasi.cost_usd | |
| logger.info(f"[STEP 2: COMPILE] Completed - Status: {kompilasi.status}, Cost: ${kompilasi.cost_usd:.6f}") | |
| if kompilasi.status == "terkumpul": | |
| logger.info(f" - Members involved: {kompilasi.jumlah_anggota}") | |
| logger.info(f" - Themes: {kompilasi.tema_utama}") | |
| elif kompilasi.error: | |
| logger.error(f" - Error: {kompilasi.error}") | |
| log(f"โ Step 2 selesai: Status {kompilasi.status}") | |
| # Step 3: Council Discussion (NEW!) | |
| council_discussion = None | |
| if kompilasi.status == "terkumpul": | |
| logger.info("[STEP 3: COUNCIL DISCUSSION] Starting multi-member deliberation...") | |
| log("๐๏ธ Step 3: Diskusi antar anggota DPR (Council)") | |
| # Get relevant members who responded | |
| responding_member_ids = [r.member_id for r in all_responses if r.error is None] | |
| relevant_members = [m for m in self.members if m.id in responding_member_ids] | |
| # Run council discussion | |
| council_discussion = await self.council_discussion_agent.invoke( | |
| aspirasi=aspirasi, | |
| responses=all_responses, | |
| members=relevant_members, | |
| discussion_rounds=2 | |
| ) | |
| total_cost += council_discussion.cost_usd | |
| logger.info(f"[STEP 3: COUNCIL DISCUSSION] Completed - Cost: ${council_discussion.cost_usd:.6f}") | |
| if council_discussion.status == "success": | |
| logger.info(f" - Rounds: {len(council_discussion.diskusi)}") | |
| logger.info(f" - Consensus: {council_discussion.konsensus}") | |
| logger.info(f" - Factions involved: {list(council_discussion.posisi_fraksi.keys())}") | |
| if council_discussion.error: | |
| logger.warning(f" - Warning: {council_discussion.error}") | |
| log(f"โ Step 3 selesai: Diskusi council dengan konsensus {council_discussion.konsensus}") | |
| else: | |
| logger.warning("[STEP 3: COUNCIL DISCUSSION] Skipped - No compilation to discuss") | |
| log("โ ๏ธ Step 3 dilewati: Tidak ada kompilasi untuk didiskusikan") | |
| # Step 4: Menindaklanjuti (Follow-up) | |
| tindak_lanjut = None | |
| if kompilasi.status == "terkumpul": | |
| logger.info("[STEP 4: FOLLOW-UP] Creating action plan...") | |
| log("๐ Step 4: Menindaklanjuti dengan rencana aksi") | |
| tindak_lanjut = await self.followup_agent.invoke(aspirasi, kompilasi) | |
| total_cost += tindak_lanjut.cost_usd | |
| logger.info(f"[STEP 4: FOLLOW-UP] Completed - Cost: ${tindak_lanjut.cost_usd:.6f}") | |
| if tindak_lanjut.komisi_penanggung_jawab: | |
| logger.info(f" - Responsible commission: {tindak_lanjut.komisi_penanggung_jawab}") | |
| logger.info(f" - Timeline: {tindak_lanjut.timeline}") | |
| if tindak_lanjut.error: | |
| logger.error(f" - Error: {tindak_lanjut.error}") | |
| log("โ Step 4 selesai") | |
| else: | |
| tindak_lanjut = TindakLanjutResponse( | |
| langkah_tindak_lanjut=[], | |
| komisi_penanggung_jawab="", | |
| timeline="", | |
| indikator_keberhasilan=[], | |
| mekanisme="", | |
| error="Tidak ada tindak lanjut karena aspirasi tidak relevan", | |
| ) | |
| logger.warning("[STEP 4: FOLLOW-UP] Skipped - No relevant responses to compile") | |
| log("โ ๏ธ Step 4 dilewati: Tidak ada tanggapan relevan") | |
| # Final summary | |
| logger.info("=" * 60) | |
| logger.info(f"PIPELINE COMPLETED - Total Cost: ${total_cost:.6f}") | |
| logger.info("=" * 60) | |
| log(f"๐ฐ Total biaya pemrosesan aspirasi: ${total_cost:.6f}") | |
| # Calculate simulation details | |
| relevansi_tinggi = sum(1 for r in all_responses if r.relevansi.lower() == "tinggi" and r.error is None) | |
| relevansi_sedang = sum(1 for r in all_responses if r.relevansi.lower() == "sedang" and r.error is None) | |
| relevansi_rendah = sum(1 for r in all_responses if r.relevansi.lower() == "rendah" and r.error is None) | |
| # Get unique factions and provinces from relevant members | |
| fraksi_set = set(m.faction for m in relevant_members) | |
| provinsi_set = set(m.province for m in relevant_members) | |
| komisi_set = set(m.komisi for m in relevant_members) | |
| # Get primary commission | |
| from .komisi_data import get_primary_komisi | |
| komisi_utama = komisi_filter if komisi_filter else get_primary_komisi(aspirasi.category) | |
| simulation_details = SimulationDetails( | |
| total_anggota_dpr=len(self.members), | |
| sample_size_requested=sample_size, | |
| anggota_relevan_terpilih=len(relevant_members), | |
| anggota_merespons=len([r for r in all_responses if r.error is None]), | |
| anggota_relevansi_tinggi=relevansi_tinggi, | |
| anggota_relevansi_sedang=relevansi_sedang, | |
| anggota_relevansi_rendah=relevansi_rendah, | |
| fraksi_terwakili=sorted(list(fraksi_set)), | |
| provinsi_terwakili=sorted(list(provinsi_set)), | |
| komisi_terwakili=sorted(list(komisi_set)), | |
| komisi_utama=komisi_utama, | |
| relevant_member_ids=[m.id for m in relevant_members], | |
| ) | |
| return PipelineResult( | |
| aspirasi=aspirasi, | |
| tanggapan_anggota=all_responses, | |
| kompilasi=kompilasi, | |
| council_discussion=council_discussion, | |
| tindak_lanjut=tindak_lanjut, | |
| simulation_details=simulation_details, | |
| timestamp=datetime.now(), | |
| total_cost_usd=total_cost, | |
| ) | |
| async def process_multiple_aspirasi( | |
| self, | |
| aspirasi_list: List[Aspirasi], | |
| sample_size: int = None, | |
| progress_callback: Optional[Callable[[str], None]] = None, | |
| ) -> List[PipelineResult]: | |
| """ | |
| Process multiple aspirations sequentially. | |
| Args: | |
| aspirasi_list: List of aspirations to process | |
| sample_size: Number of members to sample per aspiration | |
| progress_callback: Optional callback for progress updates | |
| Returns: | |
| List of PipelineResult for each aspiration | |
| """ | |
| results = [] | |
| for i, aspirasi in enumerate(aspirasi_list, 1): | |
| if progress_callback: | |
| progress_callback(f"\n{'='*60}\nAspirasi {i}/{len(aspirasi_list)}\n{'='*60}") | |
| result = await self.process_aspirasi(aspirasi, sample_size, progress_callback) | |
| results.append(result) | |
| return results | |