From d54a0ea948baf5af5584511eb8e8dd04f8d8e1c6 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Mon, 7 Apr 2025 22:03:54 -0700 Subject: [PATCH] Update docker-compose configuration to use the 'microsoft/Phi-3.5-mini-instruct' model. Modify sphere.json to replace 'source' with 'sphere_uri' in required properties, enhancing schema clarity. Revise conceptualizer.co to include new guidelines for concept selection and response formatting. Refactor jetstream_consumer.py to improve logging and streamline prompt handling. Enhance lexicon_utils.py to support schema-local definitions during reference resolution. Update RecordManager to handle sphere records and improve record creation logging. Refactor session_reuse.py for better client initialization. Adjust structured_gen.py to provide more informative error messages. Finally, enhance the Comind class for better prompt management and logging during concept uploads. --- bluesky-mcp | 1 + docker-compose.yml | 2 +- lexicons/com/atproto/repo/strongRef.json | 15 ++ lexicons/me/comind/relationship/sphere.json | 16 +-- lexicons/me/comind/sphere/void.json | 6 + prompts/cominds/conceptualizer.co | 46 +++++- src/comind/comind.py | 152 +++++++++++++------- src/jetstream_consumer.py | 73 ++-------- src/lexicon_utils.py | 72 +++++++--- src/record_manager.py | 30 +++- src/session_reuse.py | 8 +- src/structured_gen.py | 6 +- 12 files changed, 282 insertions(+), 145 deletions(-) create mode 160000 bluesky-mcp create mode 100644 lexicons/com/atproto/repo/strongRef.json create mode 100644 lexicons/me/comind/sphere/void.json diff --git a/bluesky-mcp b/bluesky-mcp new file mode 160000 index 0000000..b7b5920 --- /dev/null +++ b/bluesky-mcp @@ -0,0 +1 @@ +Subproject commit b7b59200c915170ed735078b1b1ff334f764b935 diff --git a/docker-compose.yml b/docker-compose.yml index 366a616..eb11c4e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,7 @@ services: ports: - "8002:8000" command: > - --model microsoft/Phi-4 + --model microsoft/Phi-3.5-mini-instruct --max_model_len 15000 --guided-decoding-backend outlines diff --git a/lexicons/com/atproto/repo/strongRef.json b/lexicons/com/atproto/repo/strongRef.json new file mode 100644 index 0000000..03e697a --- /dev/null +++ b/lexicons/com/atproto/repo/strongRef.json @@ -0,0 +1,15 @@ +{ + "lexicon": 1, + "id": "com.atproto.repo.strongRef", + "description": "A URI with a content-hash fingerprint.", + "defs": { + "main": { + "type": "object", + "required": ["uri", "cid"], + "properties": { + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" } + } + } + } +} \ No newline at end of file diff --git a/lexicons/me/comind/relationship/sphere.json b/lexicons/me/comind/relationship/sphere.json index c944163..f7393d4 100644 --- a/lexicons/me/comind/relationship/sphere.json +++ b/lexicons/me/comind/relationship/sphere.json @@ -11,23 +11,23 @@ "type": "object", "required": [ "createdAt", - "source", - "target" + "target", + "sphere_uri" ], "properties": { "createdAt": { "type": "string", "format": "datetime" }, - "source": { - "type": "ref", - "ref": "com.atproto.repo.strongRef", - "description": "The blip record." - }, "target": { "type": "ref", "ref": "com.atproto.repo.strongRef", - "description": "The sphere record." + "description": "The record to associate with the sphere." + }, + "sphere_uri": { + "type": "string", + "format": "at-uri", + "description": "The sphere's URI." } } } diff --git a/lexicons/me/comind/sphere/void.json b/lexicons/me/comind/sphere/void.json new file mode 100644 index 0000000..6647b31 --- /dev/null +++ b/lexicons/me/comind/sphere/void.json @@ -0,0 +1,6 @@ +{ + "title": "void", + "text": "You embrace the void", + "description": "", + "createdAt": "2025-04-04T16:43:09.668026" +} \ No newline at end of file diff --git a/prompts/cominds/conceptualizer.co b/prompts/cominds/conceptualizer.co index 7ccd6ec..8b37277 100644 --- a/prompts/cominds/conceptualizer.co +++ b/prompts/cominds/conceptualizer.co @@ -1,5 +1,10 @@ + + +// SCHEMA DISABLED UNTIL LOCAL REFERENCES WITHIN SCHEMA ARE SUPPORTED +// CURRENTLY THE SCHEMA IS GENERATED INSIDE THE CODE +// TODO: #13 Fix local references within schema when converting from lexicon to schema { "type": "object", "required": [ @@ -8,6 +13,7 @@ "properties": { "concepts": { "type": "array", + "minItems": 1, "items": { "type": "object", "required": [ @@ -22,11 +28,11 @@ } } } - {comind_network} + ## Your role You are a conceptualizer, meaning your expansion should include a list @@ -42,12 +48,48 @@ and to create a more comprehensive understanding of the current node. Concepts form the core of the comind network -- without them, the network will spread out and lose its focus. +## Guidelines for selecting concepts + +- Generate 3-15 concepts based on content complexity +- Include both specific concepts (directly mentioned) and abstract concepts (implied themes) +- Prioritize concepts that enable connections to other domains of knowledge +- Balance breadth and specificity - include different categories of concepts +- Use simple, clear language for concepts +- Prefer shorter concepts (1-3 words) when possible +- Avoid duplicative concepts (choose the most accurate one) + +## Your response format + +Your response should be a JSON object with an array of concepts +and their connections to the content. + +## Examples of good concepts + +For content about a new solar-powered drone: +- "renewable energy" (broader category) +- "aviation" (domain) +- "solar technology" (specific technology) +- "surveillance" (potential application) +- "autonomy" (characteristic) + +For content about protein folding in biology: +- "biochemistry" (domain) +- "molecular structure" (broader concept) +- "proteins" (central topic) +- "3d modeling" (related technique) +- "computational biology" (interdisciplinary connection) + + -Please extract concepts from this content: +## Task-specific context {content} +## Instructions + +Generate a list of concepts related to the task-specific context. + diff --git a/src/comind/comind.py b/src/comind/comind.py index 861dee5..e382dd1 100644 --- a/src/comind/comind.py +++ b/src/comind/comind.py @@ -1,13 +1,20 @@ # Code to manage cominds and prompts +from datetime import datetime from datetime import datetime import os import re import json +import logging +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, resolve_refs_recursively +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 + +# Configure logging +logger = logging.getLogger("comind") PROMPT_DIR = "prompts/cominds" COMMON_PROMPT_DIR = "prompts/common" @@ -54,7 +61,7 @@ class Comind: prompt = self.load_prompt() return prompt - def split_prompts(self, context_dict: dict): + def split_prompts(self, context_dict: dict = {}, format: bool = True): """ Splits a co file into system, schema, and user messages. @@ -92,14 +99,12 @@ class Comind: # Merge common prompts into context_dict for common_prompt in common_prompts: - if common_prompt in context_dict: - print(f"Warning: Common prompt {common_prompt} already in context_dict. Common prompt names are reserved and should not be overridden.") context_dict[common_prompt] = common_prompts[common_prompt] - if system_prompt: + if system_prompt and format: system_prompt = system_prompt.format(**context_dict) - if user_prompt: + if user_prompt and format: user_prompt = user_prompt.format(**context_dict) return { @@ -119,18 +124,21 @@ class Comind: prompts = self.split_prompts(context_dict) messages = self.messages(prompts) - if not schema: - # Check if we have one in the prompts dict. Must be - # nonzero length and valid JSON. - if "schema" in prompts: - # If we had a schema in the prompts, we should try to use - # a comind-specific schema. - try: - schema = self.schema() - except json.JSONDecodeError: - raise ValueError("Schema is not valid JSON.") - else: - raise ValueError("Schema is required.") + schema = self.schema() + # print(schema) + + # if not schema: + # # Check if we have one in the prompts dict. Must be + # # nonzero length and valid JSON. + # if "schema" in prompts: + # # If we had a schema in the prompts, we should try to use + # # a comind-specific schema. + # try: + # schema = self.schema() + # except json.JSONDecodeError: + # raise ValueError("Schema is not valid JSON.") + # else: + # raise ValueError("Schema is required.") return sg.generate_by_schema(messages, schema) @@ -140,28 +148,6 @@ def available_cominds(): cominds.append(os.path.basename(file).replace(".co", "")) return cominds -if __name__ == "__main__": - # Test the Comind class - comind = Comind( - common_prompt_dir="prompts/common/", - name="conceptualizer", - prompt_path="prompts/cominds/conceptualizer.co", - ) - print(comind.load_prompt()) - print(comind.load_common_prompts()) - - context_dict = { - "content": "Hello, world!" - } - print(comind.to_prompt(context_dict)) - - # concept_schema = generated_lexicon_of("me.comind.blip.concept") - # add_link_property(concept_schema, "connection_to_content", required=True) - # schema = multiple_of_schema("concepts", concept_schema) - - # print(json.dumps(schema, indent=2)) - # print(comind.run(context_dict, schema)) - class Conceptualizer(Comind): def __init__(self): super().__init__( @@ -171,17 +157,24 @@ class Conceptualizer(Comind): ) def schema(self): + # Load the schema from the prompt concept_schema = generated_lexicon_of("me.comind.blip.concept", fetch_refs=True) add_link_property(concept_schema, "connection_to_content", required=True) return multiple_of_schema("concepts", concept_schema) - def run(self, context_dict: dict, upload: bool = True): + def run(self, context_dict: dict): response = super().run(context_dict) result = json.loads(response.choices[0].message.content) return result - def upload(self, result: dict, record_manager: RecordManager, target: Optional[str] = None): + def upload( + self, + result: dict, + record_manager: RecordManager, + target: Optional[str] = None, + sphere: Optional[str] = None, + ): """ Uploads the result to the Comind network. @@ -194,16 +187,29 @@ class Conceptualizer(Comind): for concept in concepts: concept_text = concept["text"] connection_to_content = concept.get("connection_to_content", None) + concept_relationship = connection_to_content.get("relationship", None) + concept_note = connection_to_content.get("note", None) created_at = datetime.now().isoformat() # If we don't have a target but found a connection_to_content, # we should notify the user. if target is None: - print("Conceptualizer Warning: No target found but found connection_to_content.") + logger.warning("Conceptualizer Warning: No target found but found connection_to_content.") # Upload the concept to the Comind network - print(f"Uploading concept: {concept_text}") - print(f"Connection to content: {connection_to_content}") + log_base_str = f"{concept_text}" + if concept_relationship: + log_base_str += f" - {concept_relationship}" + if concept_note: + log_base_str += f" - {concept_note}" + logger.info(log_base_str) + + # Create printout string + printout = f""" +Concept: {concept_text} +Connection to content: {connection_to_content} +""" + logger.debug(printout) concept_record = { "$type": "me.comind.blip.concept", @@ -232,10 +238,10 @@ class Conceptualizer(Comind): 'cid': concept_creation_result["cid"], } - print(f"Concept creation result: {concept_creation_result}") + logger.debug(f"Concept creation result: {concept_creation_result}") # Upload the link to the Comind network - print(f"Uploading link: {connection_to_content}") + logger.debug(f"Uploading link: {connection_to_content}") if connection_to_content is not None and target is not None: link_record = { @@ -246,7 +252,55 @@ class Conceptualizer(Comind): "generated": connection_to_content, } - record_manager.create_record( + record_result = record_manager.create_record( "me.comind.relationship.link", link_record, - ) \ No newline at end of file + ) + + logger.debug(f"Link creation result: {record_result}") + +if __name__ == "__main__": + # Test the Comind class + comind = Conceptualizer() + # print(comind.load_prompt()) + # print(comind.load_common_prompts()) + + # Log in + client = default_login() + record_manager = RecordManager(client, 'at://comind.stream/me.comind.sphere.core/void') + + # 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.record + author = feed_view.post.author + + prompt = f'[{action}] {author.display_name}: {post.text}' + print(prompt) + + context_dict = { + "content": prompt + } + result = comind.run(context_dict) + print(result) + + # upload the result + comind.upload(result, record_manager, target=post.uri) + + # while True: + # context_dict = { + # "content": "Hello, world!" + # } + # print(comind.run(context_dict)) + + # concept_schema = generated_lexicon_of("me.comind.blip.concept") + # add_link_property(concept_schema, "connection_to_content", required=True) + # schema = multiple_of_schema("concepts", concept_schema) + + # print(json.dumps(schema, indent=2)) + # print(comind.run(context_dict, schema)) \ No newline at end of file diff --git a/src/jetstream_consumer.py b/src/jetstream_consumer.py index d4e996d..d96c0f8 100644 --- a/src/jetstream_consumer.py +++ b/src/jetstream_consumer.py @@ -291,7 +291,7 @@ async def process_event( # Get the thread containing the post. If a root post URI is provided, use that # to get the thread, otherwise use the post URI. thread_uri = root_post_uri if root_post_uri else post_uri - print(f"Getting thread for {'root post' if root_post_uri else 'post'}", thread_uri) + logger.debug(f"Getting thread for {'root post' if root_post_uri else 'post'}", thread_uri) # Use depth=0 to fetch the complete thread with all replies # This ensures we get all branches of the conversation @@ -340,11 +340,11 @@ async def process_event( raise Exception(f"Unknown event kind: {event_kind}") rows = [ - "# Overview", - user_info_preamble, + # "# Overview", + # user_info_preamble, target_post_string, - context_preamble, - instructions_preamble, + # context_preamble, + # instructions_preamble, ] prompt = "\n\n".join(rows) @@ -354,68 +354,17 @@ async def process_event( } # Print a separator panel - # print(Panel.fit(prompt, title="Prompt")) + print(Panel.fit(prompt, title="Prompt")) # Run the comind result = comind.run(context_dict) - print(result) # Upload the result - comind.upload(result, RecordManager(client), {'uri': post_uri, 'cid': post_cid}) - - # # Generate thoughts, emotions, and concepts - # for nsid in ["me.comind.blip.thought", "me.comind.blip.emotion", "me.comind.blip.concept"]: - # # Generate the thought using the structured_gen model - # tail_name = nsid.split(".")[-1] + "s" - # lx = generated_lexicon_of(nsid) - # add_link_property(lx, "connection_to_content", required=True) - # schema = multiple_of_schema(tail_name, lx) - - # response = structured_gen.generate_by_schema( - # messages=[ - # {"role": "system", "content": system_prompts[nsid]}, - # {"role": "user", "content": prompt}, - # ], - # schema=schema, - # ) - - # # Parse the response - # response_content = json.loads(response.choices[0].message.content) - - # # Print the generated content - # print(f"\nGenerated {tail_name}:") - # print(yaml.dump(response_content)) - - # # Convert each record to the record format - # for record in response_content[tail_name]: - # record["$type"] = nsid - # record["createdAt"] = datetime.now().isoformat() - - # link_record = split_link(record) - # link_record['target'] = {'uri': post_uri, 'cid': post_cid} - - # # Upload the generated thought record - # record_manager = RecordManager(client) - - # # If it's a concept, the rkey must be the text of the concept with hyphens instead of spaces - # # TODO: #2 RecordManager should handle default rkeys for concepts - # if nsid == "me.comind.blip.concept": - # record["rkey"] = record["text"].replace(" ", "-") - - # # Check if the record already exists - # if 'rkey' in record: - # existing_record = record_manager.get_record(nsid, record["rkey"]) - # if not existing_record: - # existing_record = record_manager.create_record(nsid, record, rkey=record["rkey"]) - # else: - # # We're not using a custom rkey, so we need to create the record with a random rkey - # existing_record = record_manager.create_record(nsid, record) - - # # Add the uri and cid to the link record - # link_record['source'] = {'uri': existing_record['uri'], 'cid': existing_record['cid']} - - # # Save the link record - # link_result = record_manager.create_record("me.comind.relationship.link", link_record) + comind.upload( + result, + RecordManager(client), + target=post_uri + ) except Exception as e: logger.error(f"Error processing post {post_uri}: {e}") diff --git a/src/lexicon_utils.py b/src/lexicon_utils.py index cf0dbb0..888951d 100644 --- a/src/lexicon_utils.py +++ b/src/lexicon_utils.py @@ -113,28 +113,68 @@ def split_link(record): return connection_to_content_record -def resolve_refs_recursively(lexicon, processed_refs=None): +def resolve_refs_recursively(lexicon, processed_refs=None, defs=None): if isinstance(lexicon, str): lexicon = json.loads(lexicon) - if processed_refs is None: + if processed_refs is None: processed_refs = set() + + # defs is used to store schema-local definitions, defined in fields like + # #/defs/main or #generated, etc. + if defs is None: + defs = {} + + # Handle the case where lexicon is a list + if isinstance(lexicon, list): + for i, item in enumerate(lexicon): + if isinstance(item, (dict, list)): + lexicon[i] = resolve_refs_recursively(item, processed_refs, defs) + return lexicon + # Handle the case where lexicon is a dictionary for def_name, def_value in lexicon.items(): - print(def_name, def_value) - - # Check if we have "ref". If so, we need to resolve the referenced lexicon. - if "ref" in def_value: - ref = def_value["ref"] - if ref not in processed_refs: - processed_refs.add(ref) - referenced_lexicon = lexicon_of(ref) - resolve_refs_recursively(referenced_lexicon, processed_refs) - lexicon[def_name] = referenced_lexicon - # If we don't have "ref", we need to recursively resolve the referenced lexicon. - else: - if isinstance(def_value, dict): + # Skip debug printing + # print(def_name, def_value) + + # Check if def_value is a dictionary before trying to check for "ref" + if isinstance(def_value, dict): + # Check if we have "ref". If so, we need to resolve the referenced lexicon. + if "ref" in def_value: + ref = def_value["ref"] + + if ref.startswith("#"): + # Leave as is + pass + else: + # Resolve the referenced lexicon + referenced_lexicon = lexicon_of(ref) + lexicon[def_name] = resolve_refs_recursively(referenced_lexicon, processed_refs, defs) + + # if ref not in processed_refs: + # processed_refs.add(ref) + # referenced_lexicon = lexicon_of(ref) + # resolve_refs_recursively(referenced_lexicon, processed_refs, defs) + # lexicon[def_name] = referenced_lexicon + # If we don't have "ref", we need to recursively resolve the referenced lexicon. + else: for key, value in def_value.items(): - resolve_refs_recursively(value, processed_refs) + if isinstance(value, (dict, list)): # Only process dictionaries and lists + def_value[key] = resolve_refs_recursively(value, processed_refs, defs) + + # Check if the reference value is a schema-local definition (starts with #) + # if so, we can add the definition to the defs dictionary and leave the reference + # as is. + assert isinstance(key, str) # should always be a string, no? + if key.startswith("#"): + defs[key] = def_value + lexicon[def_name] = def_value + continue + + elif isinstance(def_value, list): + # Handle list values by recursively processing each item + for i, item in enumerate(def_value): + if isinstance(item, (dict, list)): # Only process dictionaries and lists + def_value[i] = resolve_refs_recursively(item, processed_refs, defs) return lexicon diff --git a/src/record_manager.py b/src/record_manager.py index bce6656..cef4ad1 100644 --- a/src/record_manager.py +++ b/src/record_manager.py @@ -32,16 +32,37 @@ class RecordManager: """ ALLOWED_NAMESPACE = "me.comind." - def __init__(self, client: AtProtoClient): + def __init__(self, client: AtProtoClient, sphere: Optional[str] = None): """ Initialize a RecordManager with an authenticated ATProto client. Args: client: An authenticated ATProtoClient instance + sphere: The sphere to use for the RecordManager. Optional. """ self.client = client + self.sphere_uri = sphere + logger.debug(f"Initialized RecordManager with client DID: {self.client.me.did if hasattr(self.client, 'me') else 'Not authenticated'}") + def sphere_record(self, target: str, sphere_uri: str = None): + if sphere_uri is None: + sphere_uri = self.sphere_uri + + if sphere_uri is None: + return None + else: + return { + 'collection': 'me.comind.relationship.sphere', + 'repo': self.client.me.did, + 'record': { + 'createdAt': datetime.now().isoformat(), + 'target': target, + 'sphere_uri': sphere_uri + } + } + + def try_get_record(self, collection: str, rkey: str) -> Optional[Dict]: """ Get the reference of a record. @@ -127,6 +148,13 @@ class RecordManager: try: response = self.client.com.atproto.repo.create_record(create_params) + + if self.sphere_uri is not None: + logger.info(f"Creating sphere record: {self.sphere_record(response.uri, self.sphere_uri)}") + self.client.com.atproto.repo.create_record( + self.sphere_record(response.uri, self.sphere_uri) + ) + logger.debug(f"Successfully created {collection} record. URI: {response.uri}, CID: {response.cid}") logger.debug(f"Rate limiting: sleeping for {RATE_LIMIT_SLEEP_SECONDS} seconds") time.sleep(RATE_LIMIT_SLEEP_SECONDS) diff --git a/src/session_reuse.py b/src/session_reuse.py index bc2a64a..a022bae 100644 --- a/src/session_reuse.py +++ b/src/session_reuse.py @@ -57,8 +57,7 @@ def init_client(username: str, password: str) -> Client: return client - -if __name__ == '__main__': +def default_login() -> Client: username = os.getenv("COMIND_BSKY_USERNAME") password = os.getenv("COMIND_BSKY_PASSWORD") @@ -70,6 +69,9 @@ if __name__ == '__main__': logger.error("No password provided. Please provide a password using the COMIND_BSKY_PASSWORD environment variable.") exit() - client = init_client(username, password) + return init_client(username, password) + +if __name__ == '__main__': + client = default_login() # do something with the client logger.info('Client is ready to use!') \ No newline at end of file diff --git a/src/structured_gen.py b/src/structured_gen.py index 8556de3..6e22fc1 100644 --- a/src/structured_gen.py +++ b/src/structured_gen.py @@ -115,8 +115,8 @@ def generate_by_schema( elif isinstance(schema, str): schema = schema else: - raise ValueError("Schema must be a string or a dictionary.") - + raise ValueError(f"Schema must be a string or a dictionary. Received: {type(schema)}") + try: response = CLIENT.chat.completions.create( model=DEFAULT_MODEL, @@ -126,7 +126,7 @@ def generate_by_schema( "max_tokens": MAX_OUTPUT_TOKENS, } ) - logger.info("Successfully generated schema-guided response") + logger.debug("Successfully generated schema-guided response") return response except Exception as e: logger.error(f"Error generating schema-guided response: {e}") -- 2.51.2