Diomedes Git
commited on
Commit
·
e857958
1
Parent(s):
458e81a
memory integration, corvus first, also some fuzzy logic for paper:query weighting, etc
Browse files
README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 💬
|
| 4 |
colorFrom: yellow
|
| 5 |
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
hf_oauth: true
|
|
|
|
| 1 |
---
|
| 2 |
+
title: cluas_huginn
|
| 3 |
emoji: 💬
|
| 4 |
colorFrom: yellow
|
| 5 |
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
hf_oauth: true
|
src/characters/corvus.py
CHANGED
|
@@ -15,12 +15,11 @@ logger = logging.getLogger(__name__)
|
|
| 15 |
|
| 16 |
class Corvus:
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
def __init__(self, use_groq=True, location="Glasgow, Scotland"):
|
| 21 |
self.name = "Corvus"
|
| 22 |
self.use_groq = use_groq
|
| 23 |
self.memory = AgentMemory()
|
|
|
|
| 24 |
|
| 25 |
if use_groq:
|
| 26 |
api_key = os.getenv("GROQ_API_KEY")
|
|
@@ -71,6 +70,28 @@ class Corvus:
|
|
| 71 |
|
| 72 |
return corvus_base_prompt + memory_context
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
async def respond(self,
|
| 75 |
message: str,
|
| 76 |
conversation_history: Optional[List[Dict]] = None) -> str:
|
|
@@ -85,8 +106,8 @@ class Corvus:
|
|
| 85 |
|
| 86 |
if "paper" in message.lower() and len(message.split()) < 10: # maybe add oyther keywords? "or study"? "or article"?
|
| 87 |
recalled = self.recall_paper(message)
|
| 88 |
-
|
| 89 |
-
|
| 90 |
|
| 91 |
|
| 92 |
messages = [{"role": "system", "content": self.get_system_prompt()}]
|
|
@@ -161,7 +182,7 @@ class Corvus:
|
|
| 161 |
"content": tool_result
|
| 162 |
})
|
| 163 |
|
| 164 |
-
#
|
| 165 |
final_response = self.client.chat.completions.create(
|
| 166 |
model=self.model,
|
| 167 |
messages=messages,
|
|
@@ -171,17 +192,10 @@ class Corvus:
|
|
| 171 |
|
| 172 |
return final_response.choices[0].message.content.strip()
|
| 173 |
|
| 174 |
-
#
|
| 175 |
return choice.message.content.strip()
|
| 176 |
|
| 177 |
-
|
| 178 |
-
"""Try to recall a paper from memory before searching"""
|
| 179 |
-
matches = self.memory.search_title(query)
|
| 180 |
-
if matches:
|
| 181 |
-
return matches[0] # return most relevant
|
| 182 |
-
return None
|
| 183 |
-
|
| 184 |
-
|
| 185 |
|
| 186 |
def _format_search_for_llm(self, results: dict) -> str:
|
| 187 |
"""Format search results into text for the LLM to read."""
|
|
|
|
| 15 |
|
| 16 |
class Corvus:
|
| 17 |
|
|
|
|
|
|
|
| 18 |
def __init__(self, use_groq=True, location="Glasgow, Scotland"):
|
| 19 |
self.name = "Corvus"
|
| 20 |
self.use_groq = use_groq
|
| 21 |
self.memory = AgentMemory()
|
| 22 |
+
|
| 23 |
|
| 24 |
if use_groq:
|
| 25 |
api_key = os.getenv("GROQ_API_KEY")
|
|
|
|
| 70 |
|
| 71 |
return corvus_base_prompt + memory_context
|
| 72 |
|
| 73 |
+
# little bit of fuzzy for the recall:
|
| 74 |
+
|
| 75 |
+
def recall_paper(self, query: str) -> Optional[Dict]:
|
| 76 |
+
"""Try to recall a paper from memory before searching"""
|
| 77 |
+
matches = self.memory.search_title(query)
|
| 78 |
+
|
| 79 |
+
if matches:
|
| 80 |
+
best = matches[0]
|
| 81 |
+
logger.debug(f"Recalled: {best['title']} (score: {best['relevance_score']:.2f})")
|
| 82 |
+
return best # return most relevant
|
| 83 |
+
|
| 84 |
+
return None
|
| 85 |
+
|
| 86 |
+
def clear_memory(self):
|
| 87 |
+
"""clears the memory (testing/fresh install purposes)"""
|
| 88 |
+
|
| 89 |
+
self.memory.memory = {}
|
| 90 |
+
self.memory._write_memory({})
|
| 91 |
+
logger.info(f"{self.name}'s memory cleared.")
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
|
| 95 |
async def respond(self,
|
| 96 |
message: str,
|
| 97 |
conversation_history: Optional[List[Dict]] = None) -> str:
|
|
|
|
| 106 |
|
| 107 |
if "paper" in message.lower() and len(message.split()) < 10: # maybe add oyther keywords? "or study"? "or article"?
|
| 108 |
recalled = self.recall_paper(message)
|
| 109 |
+
if recalled:
|
| 110 |
+
return f"Oh, I remember that one! {recalled['title']}. {recalled.get('snippet', '')} Want me to search for more details?"
|
| 111 |
|
| 112 |
|
| 113 |
messages = [{"role": "system", "content": self.get_system_prompt()}]
|
|
|
|
| 182 |
"content": tool_result
|
| 183 |
})
|
| 184 |
|
| 185 |
+
# second LLM call with search results
|
| 186 |
final_response = self.client.chat.completions.create(
|
| 187 |
model=self.model,
|
| 188 |
messages=messages,
|
|
|
|
| 192 |
|
| 193 |
return final_response.choices[0].message.content.strip()
|
| 194 |
|
| 195 |
+
# no tool use, return direct response
|
| 196 |
return choice.message.content.strip()
|
| 197 |
|
| 198 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
|
| 200 |
def _format_search_for_llm(self, results: dict) -> str:
|
| 201 |
"""Format search results into text for the LLM to read."""
|
src/cluas_mcp/common/memory.py
CHANGED
|
@@ -2,6 +2,8 @@ import json
|
|
| 2 |
from pathlib import Path
|
| 3 |
from datetime import datetime, timedelta
|
| 4 |
from typing import List, Dict, Optional
|
|
|
|
|
|
|
| 5 |
|
| 6 |
class AgentMemory:
|
| 7 |
"""
|
|
@@ -87,6 +89,33 @@ class AgentMemory:
|
|
| 87 |
if keys_to_delete:
|
| 88 |
self._write_memory(self.memory)
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
|
| 92 |
# poss usage example:
|
|
|
|
| 2 |
from pathlib import Path
|
| 3 |
from datetime import datetime, timedelta
|
| 4 |
from typing import List, Dict, Optional
|
| 5 |
+
from difflib import SequenceMatcher
|
| 6 |
+
|
| 7 |
|
| 8 |
class AgentMemory:
|
| 9 |
"""
|
|
|
|
| 89 |
if keys_to_delete:
|
| 90 |
self._write_memory(self.memory)
|
| 91 |
|
| 92 |
+
def search_titl_scored(self, query: str) -> List[Dict]:
|
| 93 |
+
"""Return items with relevance scores"""
|
| 94 |
+
query_lower = query.lower()
|
| 95 |
+
results = []
|
| 96 |
+
|
| 97 |
+
for item in self.memory.values():
|
| 98 |
+
title_lower = item["title"].lower()
|
| 99 |
+
|
| 100 |
+
similarity = SequenceMatcher(None, query_lower, title_lower).ratio()
|
| 101 |
+
|
| 102 |
+
query_words = set(query_lower.split())
|
| 103 |
+
title_words = set(title_lower.split())
|
| 104 |
+
word_overlap = len(query_words & title_words) / len(query_words) if query_words else 0
|
| 105 |
+
|
| 106 |
+
score = (similarity * 0.7) + (word_overlap * 0.3)
|
| 107 |
+
|
| 108 |
+
if score > 0.2:
|
| 109 |
+
result = item.copy()
|
| 110 |
+
result['relevance_score'] = score
|
| 111 |
+
results.append(result)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
results.sort(key=lambda x: x['relevance_socre'], reverse=True)
|
| 115 |
+
return results
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
|
| 119 |
|
| 120 |
|
| 121 |
# poss usage example:
|
src/cluas_mcp/news/news_search_entrypoint.py
CHANGED
|
@@ -96,3 +96,4 @@ def verify_claim(claim: str) -> dict:
|
|
| 96 |
|
| 97 |
|
| 98 |
|
|
|
|
|
|
| 96 |
|
| 97 |
|
| 98 |
|
| 99 |
+
|
src/cluas_mcp/observation/observation_entrypoint.py
CHANGED
|
@@ -93,3 +93,4 @@
|
|
| 93 |
|
| 94 |
|
| 95 |
|
|
|
|
|
|
| 93 |
|
| 94 |
|
| 95 |
|
| 96 |
+
|
src/cluas_mcp/web/web_search_entrypoint.py
CHANGED
|
@@ -102,3 +102,4 @@ def get_quick_facts(topic: str) -> dict:
|
|
| 102 |
|
| 103 |
|
| 104 |
|
|
|
|
|
|
| 102 |
|
| 103 |
|
| 104 |
|
| 105 |
+
|
tests/test_memory.py
ADDED
|
File without changes
|