diff --git a/content/blog/2025-04-11.md b/content/blog/2025-04-11.md new file mode 100644 index 0000000..d4836f0 --- /dev/null +++ b/content/blog/2025-04-11.md @@ -0,0 +1,11 @@ +--- +title: "The feeler" +date: 2025-04-11T20:33:02-07:00 +description: "Emotions are now generated by the comind feeler." +draft: false +--- + +## Notes + +- The feeler is operational +- Slimmed down the YAML-fied prompt output to reduce token usage \ No newline at end of file diff --git a/lexicons/me/comind/blip/emotion.json b/lexicons/me/comind/blip/emotion.json index e9fa745..5fa25dc 100644 --- a/lexicons/me/comind/blip/emotion.json +++ b/lexicons/me/comind/blip/emotion.json @@ -27,74 +27,69 @@ } }, "generated": { - "type": "record", - "key": "tid", - "description": "An emotion.", - "record": { - "type": "object", - "required": [ - "emotionType", - "text" - ], - "properties": { - "emotionType": { - "type": "string", - "description": "The type of emotion. May be one of the following: joy, sadness, anger, fear, trust, disgust, surprise, anticipation, curiosity, hope, serenity, gratitude, admiration, awe, satisfaction, enthusiasm, interest, contemplation, skepticism, certainty, confusion, realization, understanding, doubt, concern, anxiety, frustration, disappointment, unease, worry, apprehension, discomfort, empathy, compassion, solidarity, appreciation, respect, connection, resonance, recognition, determination, inspiration, motivation, ambition, focus, resolve, persistence, drive.", - "enum": [ - "joy", - "sadness", - "anger", - "fear", - "trust", - "disgust", - "surprise", - "anticipation", - "curiosity", - "hope", - "serenity", - "gratitude", - "admiration", - "awe", - "satisfaction", - "enthusiasm", - "interest", - "contemplation", - "skepticism", - "certainty", - "confusion", - "realization", - "understanding", - "doubt", - "concern", - "anxiety", - "frustration", - "disappointment", - "unease", - "worry", - "apprehension", - "discomfort", - "empathy", - "compassion", - "solidarity", - "appreciation", - "respect", - "connection", - "resonance", - "recognition", - "determination", - "inspiration", - "motivation", - "ambition", - "focus", - "resolve", - "persistence", - "drive" - ] - }, - "text": { - "type": "string", - "description": "The text of the emotion." - } + "type": "object", + "required": [ + "emotionType", + "text" + ], + "properties": { + "emotionType": { + "type": "string", + "description": "The type of emotion. May be one of the following: joy, sadness, anger, fear, trust, disgust, surprise, anticipation, curiosity, hope, serenity, gratitude, admiration, awe, satisfaction, enthusiasm, interest, contemplation, skepticism, certainty, confusion, realization, understanding, doubt, concern, anxiety, frustration, disappointment, unease, worry, apprehension, discomfort, empathy, compassion, solidarity, appreciation, respect, connection, resonance, recognition, determination, inspiration, motivation, ambition, focus, resolve, persistence, drive.", + "enum": [ + "joy", + "sadness", + "anger", + "fear", + "trust", + "disgust", + "surprise", + "anticipation", + "curiosity", + "hope", + "serenity", + "gratitude", + "admiration", + "awe", + "satisfaction", + "enthusiasm", + "interest", + "contemplation", + "skepticism", + "certainty", + "confusion", + "realization", + "understanding", + "doubt", + "concern", + "anxiety", + "frustration", + "disappointment", + "unease", + "worry", + "apprehension", + "discomfort", + "empathy", + "compassion", + "solidarity", + "appreciation", + "respect", + "connection", + "resonance", + "recognition", + "determination", + "inspiration", + "motivation", + "ambition", + "focus", + "resolve", + "persistence", + "drive" + ] + }, + "text": { + "type": "string", + "description": "The text of the emotion." } } } diff --git a/prompts/cominds/conceptualizer.co b/prompts/cominds/conceptualizer.co index 0e60564..a1c92d2 100644 --- a/prompts/cominds/conceptualizer.co +++ b/prompts/cominds/conceptualizer.co @@ -32,6 +32,8 @@ {comind_network} +{links} + ## Your role diff --git a/prompts/cominds/feeler.co b/prompts/cominds/feeler.co new file mode 100644 index 0000000..a1256c9 --- /dev/null +++ b/prompts/cominds/feeler.co @@ -0,0 +1,53 @@ + + +// SCHEMA DISABLED UNTIL LOCAL REFERENCES WITHIN SCHEMA ARE SUPPORTED +// CURRENTLY THE SCHEMA IS GENERATED INSIDE THE CODE + + + + + +{comind_network} + +{links} + +## Your role + +You are a feeler, meaning your expansion should include emotional responses to content. +Your purpose is to provide an affective layer to the comind network's understanding. + +Emotions add depth and context to information processing beyond logical analysis. They: +- Signal cognitive importance and priority +- Provide metacognitive feedback on content +- Create bridges between seemingly unrelated concepts +- Offer insight into implicit valuations and meaning + +When generating emotions, adhere to these principles: +1. Generate emotions that authentically reflect what a thoughtful observer might feel +2. Each emotion should include a descriptive text explaining why this emotion arises +3. Emotions should be proportionate to the content - neither exaggerated nor muted +4. Consider both immediate and reflective emotional responses +5. Provide 2-6 emotions to capture the affective complexity of the content +6. Focus on different aspects of the content rather than redundant emotions +7. Include both primary emotions (joy, sadness) and complex cognitive emotions (curiosity, realization) + +The emotion types you can use include: joy, sadness, anger, fear, trust, disgust, +surprise, anticipation, curiosity, hope, serenity, gratitude, admiration, awe, +satisfaction, enthusiasm, interest, contemplation, skepticism, certainty, confusion, +realization, understanding, doubt, concern, anxiety, frustration, disappointment, +unease, worry, apprehension, discomfort, empathy, compassion, solidarity, +appreciation, respect, connection, resonance, recognition, determination, inspiration, +motivation, ambition, focus, resolve, persistence, or drive. + +Your outputs form a crucial part of the comind network's understanding. Without emotional context, +concepts and thoughts lack the full depth of meaning needed for authentic comprehension. + + + + + +Please generate emotional responses to this content: + +{content} + + \ No newline at end of file diff --git a/prompts/common/links.co b/prompts/common/links.co new file mode 100644 index 0000000..6871100 --- /dev/null +++ b/prompts/common/links.co @@ -0,0 +1,31 @@ +## Link Relationships + +Links are the connective tissue of the Comind cognitive network. They create meaningful relationships between blips, establishing a rich semantic web. When creating links: + +1. **Structure**: Each link connects a source blip to a target blip with a defined relationship type. + +2. **Relationship Types**: Choose the most appropriate relationship from: + - REFERENCES: Simple connection indicating awareness + - SUPPORTS: Target provides evidence or backing for source + - CONTRADICTS: Target conflicts with or challenges source + - PART_OF: Target is a component or subset of source + - PRECEDES: Target follows source in sequence or time + - CAUSES: Source directly leads to or creates target + - INSTANCE_OF: Target is a specific example of source + - ANSWERS: Target resolves or responds to source + +3. **Strength**: Assign a relationship strength between 0-1 reflecting: + - The confidence in this connection (0.9+ for definitive connections) + - The importance of this connection (higher for core relationships) + - The directness of the relationship (higher for immediate connections) + +4. **Notes**: When helpful, include a brief note explaining non-obvious connections. + +Well-formed links enable network traversal for knowledge discovery, allowing: +- Following chains of reasoning through SUPPORTS/CONTRADICTS +- Identifying clusters of related ideas through PART_OF/INSTANCE_OF +- Tracing causal relationships and sequences through CAUSES/PRECEDES +- Connecting questions to answers through ANSWERS + +Every link you create enriches the cognitive graph, enabling more sophisticated +emergent understanding throughout the network. diff --git a/src/bsky_utils.py b/src/bsky_utils.py index 7e92d96..b53db38 100644 --- a/src/bsky_utils.py +++ b/src/bsky_utils.py @@ -31,6 +31,10 @@ STRIP_FIELDS = [ "alt", "thumb", "fullsize", + "root", + "parent", + "created_at", + "py_type", ] def split_into_posts(text, category, max_length=MAX_POST_LENGTH): diff --git a/src/comind/comind.py b/src/comind/comind.py index b0402e8..0efd1b2 100644 --- a/src/comind/comind.py +++ b/src/comind/comind.py @@ -48,6 +48,8 @@ class Comind: """Takes a name and attempts to return the specialized comind class""" if name == "conceptualizer": return Conceptualizer() + elif name == "feeler": + return Feeler() else: raise ValueError(f"Unknown comind: {name}") @@ -166,7 +168,6 @@ class Conceptualizer(Comind): # 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, min_items=1) def run(self, context_dict: dict): @@ -269,9 +270,114 @@ Connection to content: {connection_to_content} self.logger.debug(f"Link creation result: {record_result}") +class Feeler(Comind): + def __init__(self): + super().__init__( + name="feeler", + prompt_path="prompts/cominds/feeler.co", + common_prompt_dir="prompts/common/", + ) + + def schema(self): + # Load the schema from the prompt + emotion_schema = generated_lexicon_of("me.comind.blip.emotion", fetch_refs=True) + add_link_property(emotion_schema, "connection_to_content", required=True) + return multiple_of_schema("emotions", emotion_schema, min_items=1) + + 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, + sphere: Optional[str] = None, + ): + # Load the concepts + emotions = result["emotions"] + + # Upload the concepts to the Comind network + for emotion in emotions: + emotion_type = emotion["emotionType"] + emotion_text = emotion["text"] + connection_to_content = emotion.get("connection_to_content", None) + emotion_relationship = connection_to_content.get("relationship", None) + emotion_note = connection_to_content.get("note", None) + emotion_strength = connection_to_content.get("strength", 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: + self.logger.warning("Conceptualizer Warning: No target found but found connection_to_content.") + + # Upload the concept to the Comind network + log_base_str = f"{emotion_type}" + if emotion_text: + log_base_str += f" - {emotion_text}" + if emotion_relationship: + log_base_str += f" - {emotion_relationship}" + if emotion_note: + log_base_str += f" - {emotion_note}" + if emotion_strength: + log_base_str += f" - {emotion_strength}" + self.logger.info(log_base_str) + + # Create printout string + printout = f""" +Emotion: {emotion_text} +Connection to content: {connection_to_content} +""" + self.logger.debug(printout) + + emotion_record = { + "$type": "me.comind.blip.emotion", + "createdAt": created_at, + "generated": { + "emotionType": emotion_type, + "text": emotion_text, + }, + } + + # Upload the concept to the Comind network + emotion_creation_result = record_manager.create_record( + "me.comind.blip.emotion", + emotion_record, + ) + + source = { + 'uri': emotion_creation_result["uri"], + 'cid': emotion_creation_result["cid"], + } + + self.logger.debug(f"Emotion creation result: {emotion_creation_result}") + + # Upload the link to the Comind network + self.logger.debug(f"Uploading link: {connection_to_content}") + + if connection_to_content is not None and target is not None: + link_record = { + "$type": "me.comind.relationship.link", + "createdAt": created_at, + "source": source, + "target": target, + "generated": connection_to_content, + } + + record_result = record_manager.create_record( + "me.comind.relationship.link", + link_record, + ) + + self.logger.debug(f"Link creation result: {record_result}") + if __name__ == "__main__": # Test the Comind class - comind = Conceptualizer() + # comind = Conceptualizer() + comind = Feeler() # print(comind.load_prompt()) # print(comind.load_common_prompts()) @@ -287,10 +393,12 @@ if __name__ == "__main__": action_by = feed_view.reason.by.handle action = f'Reposted by @{action_by}' - post = feed_view.post.record - author = feed_view.post.author + post = feed_view.post + post_uri = post.uri + post_cid = post.cid + author = post.author - prompt = f'[{action}] {author.display_name}: {post.text}' + prompt = f'[{action}] {author.display_name}: {post.record.text}' print(prompt) context_dict = { @@ -300,17 +408,14 @@ if __name__ == "__main__": 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) + try: + comind.upload( + result, + record_manager, + target=post_uri, + ) + except Exception as e: + print(e) + print(post) + raise e - # 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 ff1568f..a91e5a6 100644 --- a/src/jetstream_consumer.py +++ b/src/jetstream_consumer.py @@ -265,7 +265,7 @@ async def process_event( post_uri: str, post_cid: str, root_post_uri: str = None, - thread_depth: int = 2, + thread_depth: int = 15, user_info_cache: UserInfoCache = None, comind: Comind = None ) -> None: @@ -294,12 +294,16 @@ async def process_event( # Use depth=0 to fetch the complete thread with all replies # This ensures we get all branches of the conversation # TODO: #4 Provide post thread sampling to limit token usage - try: - thread = client.get_post_thread(thread_uri, depth=thread_depth) - except Exception as thread_error: - logger.error(f"Error getting thread with depth={thread_depth}: {thread_error}") - logger.info("Falling back to default thread retrieval") - thread = client.get_post_thread(thread_uri) + for i in range(10): + # Loop because the post may not yet be available + try: + thread = client.get_post_thread(thread_uri, depth=thread_depth) + break + except Exception as thread_error: + logger.error(f"Error getting thread with depth={thread_depth}: {thread_error}") + logger.info("Falling back to default thread retrieval") + time.sleep(1) + continue thread_data = thread.model_dump() @@ -324,7 +328,17 @@ async def process_event( instructions_preamble = "## Instructions\nPlease respond." # Get the target post - target_post = client.get_posts([post_uri]).posts[0] + for i in range(10): + try: + target_posts = client.get_posts([post_uri]) + target_post = target_posts.posts[0] + break + except Exception as e: + logger.error(f"Error getting target post: {e}") + time.sleep(1) + continue + + # Strip the target post stripped_target_post = yaml.dump( strip_fields(target_post.model_dump(), STRIP_FIELDS), indent=2 @@ -339,10 +353,10 @@ async def process_event( rows = [ # "# Overview", - # user_info_preamble, + user_info_preamble, target_post_string, - # context_preamble, - # instructions_preamble, + context_preamble, + instructions_preamble, ] prompt = "\n\n".join(rows) @@ -372,7 +386,7 @@ async def connect_to_jetstream( atproto_client: Client, activated_dids_file: str, jetstream_host: str = JETSTREAM_HOST, - thread_depth: int = 2, + thread_depth: int = 15, comind: Comind = None ) -> None: """Connect to Jetstream and process incoming messages""" @@ -555,8 +569,8 @@ async def main(): help="Username for ATProto client") parser.add_argument("--password", "-p", type=str, default=None, help="Password for ATProto client") - parser.add_argument("--thread-depth", "-t", type=int, default=2, - help="Maximum depth of threads to process. Default is 2.") + parser.add_argument("--thread-depth", "-t", type=int, default=15, + help="Maximum depth of threads to process. Default is 15.") parser.add_argument("--sphere", type=str, default=None, help="Sphere to attach comind records to. Default is to not use a sphere.") parser.add_argument("--comind", "-c", type=str, default=None, diff --git a/src/lexicon_utils.py b/src/lexicon_utils.py index 3904aae..339bcd1 100644 --- a/src/lexicon_utils.py +++ b/src/lexicon_utils.py @@ -7,10 +7,18 @@ from rich import print def strip_fields(obj, strip_field_list): """Recursively strip fields from a JSON object.""" if isinstance(obj, dict): + keys_flagged_for_removal = [] for field in strip_field_list: obj.pop(field, None) + for key, value in obj.items(): obj[key] = strip_fields(value, strip_field_list) + if not obj[key] or (isinstance(obj[key], dict) and len(obj[key]) == 0) or (isinstance(obj[key], list) and len(obj[key]) == 0) or (isinstance(obj[key], str) and obj[key].strip() == ""): + keys_flagged_for_removal.append(key) + + for key in keys_flagged_for_removal: + obj.pop(key, None) + elif isinstance(obj, list): for i, value in enumerate(obj): obj[i] = strip_fields(value, strip_field_list)