diff --git a/monitor.py b/monitor.py new file mode 100644 index 0000000..6f3aae1 --- /dev/null +++ b/monitor.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +# Simple fasthtml app to monitor comind records + +import os +from typing import Dict, List, Optional +import datetime +from atproto_client import Client as AtProtoClient +from dotenv import load_dotenv +from fasthtml.common import * + +# Load environment variables +load_dotenv() + +# ATProto credentials +BSKY_USERNAME = os.getenv("COMIND_BSKY_USERNAME") +BSKY_PASSWORD = os.getenv("COMIND_BSKY_PASSWORD") + +# Define collections to monitor +COLLECTIONS = [ + "me.comind.sphere.core", + "me.comind.blip.thought", + "me.comind.blip.emotion", + "me.comind.blip.concept", + "me.comind.meld.request", + "me.comind.meld.response" +] + +# Create FastHTML app +app, rt = fast_app() + +def format_datetime(dt_str: str) -> str: + """Format datetime string to more readable format""" + dt = datetime.datetime.fromisoformat(dt_str.replace("Z", "+00:00")) + return dt.strftime("%Y-%m-%d %H:%M:%S") + +def init_client() -> AtProtoClient: + """Initialize ATProto client with credentials""" + if not BSKY_USERNAME or not BSKY_PASSWORD: + raise ValueError( + "No credentials provided. Please set COMIND_BSKY_USERNAME and " + "COMIND_BSKY_PASSWORD environment variables." + ) + + client = AtProtoClient() + client.login(BSKY_USERNAME, BSKY_PASSWORD) + return client + +def get_recent_records(client: AtProtoClient, collection: str, limit: int = 10) -> List[Dict]: + """Get recent records from a collection""" + try: + response = client.com.atproto.repo.list_records({ + 'collection': collection, + 'repo': client.me.did, + 'limit': limit + }) + return response.records + except Exception as e: + print(f"Error listing records in collection {collection}: {str(e)}") + return [] + +def render_record_card(record, collection): + """Render a record as a card""" + record_data = record.value + uri_parts = record.uri.split('/') + rkey = uri_parts[-1] + + # Extract record type-specific information + title = "" + body = "" + + if collection == "me.comind.sphere.core": + title = record_data.get("title", "Untitled Sphere") + body = record_data.get("text", "") + elif collection == "me.comind.blip.thought": + title = f"Thought: {record_data.get('generated', {}).get('thoughtType', 'Unknown')}" + body = record_data.get('generated', {}).get('text', '') + elif collection == "me.comind.blip.emotion": + title = f"Emotion: {record_data.get('generated', {}).get('emotionType', 'Unknown')}" + body = record_data.get('generated', {}).get('text', '') + elif collection == "me.comind.blip.concept": + title = "Concept" + body = record_data.get('generated', {}).get('text', '') + elif collection == "me.comind.meld.request": + title = f"Meld Request: {record_data.get('generated', {}).get('requestType', 'Unknown')}" + body = record_data.get('generated', {}).get('prompt', '') + elif collection == "me.comind.meld.response": + title = "Meld Response" + body = record_data.get('generated', {}).get('content', '') + + # Format created date if available + created_at = record_data.get("createdAt", "") + created_formatted = format_datetime(created_at) if created_at else "" + + return Div( + H3(title), + P(body) if body else Div(), + Small(f"Created: {created_formatted}"), + Small(f"ID: {rkey}"), + cls="card", + style="margin-bottom: 1rem; padding: 1rem; border: 1px solid #ddd; border-radius: 5px;" + ) + +@rt("/") +def get(): + """Main page showing all recent records""" + try: + client = init_client() + + collection_sections = [] + + for collection in COLLECTIONS: + records = get_recent_records(client, collection, limit=5) + + # Skip empty collections + if not records: + continue + + # Create section for this collection + collection_name = collection.split(".")[-1].capitalize() + record_cards = [render_record_card(record, collection) for record in records] + + collection_section = Div( + H2(f"{collection_name} Records"), + *record_cards, + cls="collection-section", + style="margin-bottom: 2rem;" + ) + + collection_sections.append(collection_section) + + # If no records found + if not collection_sections: + collection_sections = [P("No records found. Please check your credentials and collection names.")] + + # Refresh button + refresh_button = Button("Refresh", hx_get="/", hx_swap="outerHTML", hx_target="#content") + + return Title("Comind Record Monitor"), Div( + H1("Comind Record Monitor"), + refresh_button, + Div(*collection_sections, id="content"), + cls="container" + ) + except Exception as e: + return Title("Error"), Div( + H1("Error"), + P(f"An error occurred: {str(e)}"), + cls="container" + ) + +@rt("/{collection}") +def get_collection(collection: str): + """View records for a specific collection""" + try: + client = init_client() + records = get_recent_records(client, f"me.comind.{collection}", limit=10) + + record_cards = [render_record_card(record, f"me.comind.{collection}") for record in records] + + if not record_cards: + record_cards = [P("No records found in this collection.")] + + return Title(f"{collection.capitalize()} Records"), Div( + H1(f"{collection.capitalize()} Records"), + A("Back to All Records", href="/", cls="button"), + Div(*record_cards, cls="records"), + cls="container" + ) + except Exception as e: + return Title("Error"), Div( + H1("Error"), + P(f"An error occurred: {str(e)}"), + A("Back to All Records", href="/", cls="button"), + cls="container" + ) + +# Start the app when run directly +if __name__ == "__main__": + serve() + + + diff --git a/requirements-monitor.txt b/requirements-monitor.txt new file mode 100644 index 0000000..929829f --- /dev/null +++ b/requirements-monitor.txt @@ -0,0 +1,40 @@ +annotated-types==0.7.0 +anyio==4.9.0 +apsw==3.49.1.0 +apswutils==0.0.2 +atproto==0.0.61 +beautifulsoup4==4.13.4 +certifi==2025.1.31 +cffi==1.17.1 +click==8.1.8 +cryptography==44.0.2 +dnspython==2.7.0 +fastcore==1.8.1 +fastlite==0.1.3 +h11==0.14.0 +httpcore==1.0.8 +httptools==0.6.4 +httpx==0.28.1 +idna==3.10 +itsdangerous==2.2.0 +libipld==3.0.1 +oauthlib==3.2.2 +packaging==25.0 +pycparser==2.22 +pydantic==2.11.3 +pydantic-core==2.33.1 +python-dateutil==2.9.0.post0 +python-dotenv==1.1.0 +python-fasthtml==0.12.14 +python-multipart==0.0.20 +pyyaml==6.0.2 +six==1.17.0 +sniffio==1.3.1 +soupsieve==2.7 +starlette==0.46.2 +typing-extensions==4.13.2 +typing-inspection==0.4.0 +uvicorn==0.34.2 +uvloop==0.21.0 +watchfiles==1.0.5 +websockets==13.1 diff --git a/src/comind/comind.py b/src/comind/comind.py index 708d48c..94cddac 100644 --- a/src/comind/comind.py +++ b/src/comind/comind.py @@ -6,12 +6,14 @@ import os import re import json import logging +from time import time from src.session_reuse import default_login import src.structured_gen as sg from src.lexicon_utils import generated_lexicon_of, multiple_of_schema, add_link_property from src.record_manager import RecordManager from typing import Optional from rich import print +from rich.panel import Panel from src.comind.logging_config import configure_logger_without_timestamp, configure_root_logger_without_timestamp # Configure root logger without timestamps - this affects all logging in the application @@ -546,12 +548,14 @@ Connection to content: {connection_to_content} if __name__ == "__main__": # Test the Comind class - # comind = Conceptualizer() - # comind = Feeler() - comind = Thinker() + comind1 = Conceptualizer() + comind2 = Feeler() + comind3 = Thinker() # print(comind.load_prompt()) # print(comind.load_common_prompts()) + cominds = [comind1, comind2, comind3] + # Log in client = default_login() record_manager = RecordManager(client, 'at://neuromute.ai/me.comind.sphere.core/materials') @@ -560,57 +564,112 @@ if __name__ == "__main__": # sphere_record = record_manager.get_sphere_record() # perspective = sphere_record.value['text'] + generated_strings = [] - try: - core_perspective = record_manager.get_perspective() - if not core_perspective: - raise ValueError("Core perspective was retrieved but is empty") - - # Print the first 100 characters of the core perspective for debugging - print(f"Core perspective (first 100 chars): {core_perspective[:100]}...") - except Exception as e: - comind.logger.error(f"Failed to get core perspective: {e}") - print(f"[red]Error:[/red] Failed to get core perspective: {e}") - print("Using placeholder core perspective for testing.") - core_perspective = "This is a placeholder core perspective for testing." - - # Print the common prompts that are being loaded - common_prompts = comind.load_common_prompts() - print(f"Common prompts loaded: {list(common_prompts.keys())}") - - # Get "Home" page. Use pagination (cursor + limit) to fetch all posts - timeline = client.get_timeline(algorithm='reverse-chronological') - for feed_view in timeline.feed: - action = 'New Post' - if feed_view.reason: - action_by = feed_view.reason.by.handle - action = f'Reposted by @{action_by}' - - post = feed_view.post - post_uri = post.uri - post_cid = post.cid - author = post.author - - prompt = f'[{action}] {author.display_name}: {post.record.text}' - - context_dict = { - "content": prompt, - "core_perspective": core_perspective, - } - + for comind in cominds: try: - result = comind.run(context_dict) - print(result) - - # upload the result - comind.upload( - result, - record_manager, - target=post_uri, - ) + core_perspective = record_manager.get_perspective() + if not core_perspective: + raise ValueError("Core perspective was retrieved but is empty") + + # Print the first 100 characters of the core perspective for debugging + print(f"Core perspective (first 100 chars): {core_perspective[:100]}...") except Exception as e: - comind.logger.error(f"Error processing post: {e}") - print(f"[red]Error processing post:[/red] {e}") - print(f"Post: {post}") - # Continue with next post rather than crashing - continue + comind.logger.error(f"Failed to get core perspective: {e}") + print(f"[red]Error:[/red] Failed to get core perspective: {e}") + print("Using placeholder core perspective for testing.") + core_perspective = "This is a placeholder core perspective for testing." + + # Print the common prompts that are being loaded + common_prompts = comind.load_common_prompts() + print(f"Common prompts loaded: {list(common_prompts.keys())}") + + # Types dict + blip_types = {} + + # Get "Home" page. Use pagination (cursor + limit) to fetch all posts + timeline = client.get_timeline(algorithm='reverse-chronological') + for feed_view in timeline.feed: + action = 'New Post' + if feed_view.reason: + action_by = feed_view.reason.by.handle + action = f'Reposted by @{action_by}' + + post = feed_view.post + post_uri = post.uri + post_cid = post.cid + author = post.author + + prompt = f'[{action}] {author.display_name}: {post.record.text}' + + context_dict = { + "content": prompt, + "core_perspective": core_perspective, + } + + try: + result = comind.run(context_dict) + print(result) + + if isinstance(comind, Feeler): + for emotion in result["emotions"]: + etype = emotion["emotionType"] + if etype not in blip_types: + blip_types[etype] = 0 + blip_types[etype] += 1 + elif isinstance(comind, Thinker): + for thought in result["thoughts"]: + ttype = thought["thoughtType"] + if ttype not in blip_types: + blip_types[ttype] = 0 + blip_types[ttype] += 1 + elif isinstance(comind, Conceptualizer): + for concept in result["concepts"]: + ctype = concept["text"] + if ctype not in blip_types: + blip_types[ctype] = 0 + blip_types[ctype] += 1 + + # upload the result + comind.upload( + result, + record_manager, + target=post_uri, + ) + + # Print out the count of different concept types. + # print(blip_types) + + # generated text + for thing in result["concepts"]: + generated_strings.append(thing["text"]) + + + user_prompt = "Here is a list of concepts occuring currently, with the most\n" + \ + "recent at the bottom. Give me the zeitgeist.\n" + \ + "\n".join(generated_strings) + + model_statement = sg.generate_by_schema( + sg.messages(user_prompt), + """ + { + "type": "object", + "required": ["zeitgeist"], + "properties": { + "zeitgeist": {"type": "string"} + } + } + """ + ) + + new_txt = json.loads(model_statement.choices[0].message.content) + + print(Panel(new_txt['zeitgeist'])) + + time.sleep(60) + except Exception as e: + comind.logger.error(f"Error processing post: {e}") + print(f"[red]Error processing post:[/red] {e}") + print(f"Post: {post}") + # Continue with next post rather than crashing + continue