From 5dd11757d6211f38fa69babe5e0bf27a4ac022e1 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Fri, 11 Apr 2025 20:34:19 -0700 Subject: [PATCH] Add new 'feeler' comind class to enhance emotional response generation. Introduce a blog post titled "The feeler" detailing the operational status of the feeler and its impact on emotion generation. Update emotion schema in JSON to streamline properties and improve clarity. Enhance prompts to include links for better contextual relationships. --- content/blog/2025-04-11.md | 11 +++ lexicons/me/comind/blip/emotion.json | 131 ++++++++++++------------- prompts/cominds/conceptualizer.co | 2 + prompts/cominds/feeler.co | 53 ++++++++++ prompts/common/links.co | 31 ++++++ src/bsky_utils.py | 4 + src/comind/comind.py | 141 +++++++++++++++++++++++---- src/jetstream_consumer.py | 42 +++++--- src/lexicon_utils.py | 8 ++ 9 files changed, 323 insertions(+), 100 deletions(-) create mode 100644 content/blog/2025-04-11.md create mode 100644 prompts/cominds/feeler.co create mode 100644 prompts/common/links.co 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) -- 2.51.2