versionhq 1.1.10.8__py3-none-any.whl → 1.1.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. versionhq/__init__.py +1 -1
  2. versionhq/_utils/vars.py +2 -0
  3. versionhq/agent/TEMPLATES/Backstory.py +2 -2
  4. versionhq/agent/default_agents.py +15 -0
  5. versionhq/agent/model.py +127 -39
  6. versionhq/agent/parser.py +3 -20
  7. versionhq/{_utils → agent}/rpm_controller.py +22 -15
  8. versionhq/knowledge/__init__.py +0 -0
  9. versionhq/knowledge/_utils.py +11 -0
  10. versionhq/knowledge/embedding.py +192 -0
  11. versionhq/knowledge/model.py +54 -0
  12. versionhq/knowledge/source.py +413 -0
  13. versionhq/knowledge/source_docling.py +129 -0
  14. versionhq/knowledge/storage.py +177 -0
  15. versionhq/llm/model.py +76 -62
  16. versionhq/memory/__init__.py +0 -0
  17. versionhq/memory/contextual_memory.py +96 -0
  18. versionhq/memory/model.py +174 -0
  19. versionhq/storage/base.py +14 -0
  20. versionhq/storage/ltm_sqlite_storage.py +131 -0
  21. versionhq/storage/mem0_storage.py +109 -0
  22. versionhq/storage/rag_storage.py +231 -0
  23. versionhq/storage/task_output_storage.py +18 -29
  24. versionhq/storage/utils.py +26 -0
  25. versionhq/task/TEMPLATES/Description.py +5 -0
  26. versionhq/task/evaluate.py +122 -0
  27. versionhq/task/model.py +134 -43
  28. versionhq/team/team_planner.py +1 -1
  29. versionhq/tool/model.py +44 -46
  30. {versionhq-1.1.10.8.dist-info → versionhq-1.1.11.0.dist-info}/METADATA +54 -40
  31. versionhq-1.1.11.0.dist-info/RECORD +64 -0
  32. versionhq-1.1.10.8.dist-info/RECORD +0 -45
  33. {versionhq-1.1.10.8.dist-info → versionhq-1.1.11.0.dist-info}/LICENSE +0 -0
  34. {versionhq-1.1.10.8.dist-info → versionhq-1.1.11.0.dist-info}/WHEEL +0 -0
  35. {versionhq-1.1.10.8.dist-info → versionhq-1.1.11.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,131 @@
1
+ import json
2
+ import sqlite3
3
+ from pathlib import Path
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from versionhq._utils.logger import Logger
7
+ from versionhq.storage.utils import fetch_db_storage_path
8
+
9
+
10
+ class LTMSQLiteStorage:
11
+ """
12
+ An updated SQLite storage class for LTM data storage.
13
+ """
14
+
15
+ def __init__(self, db_path: Optional[str] = None) -> None:
16
+ if db_path is None:
17
+ db_path = str(Path(fetch_db_storage_path()) / "ltm_storage.db")
18
+
19
+ self.db_path = db_path
20
+ self._logger: Logger = Logger(verbose=True)
21
+
22
+ Path(self.db_path).parent.mkdir(parents=True, exist_ok=True)
23
+ self._initialize_db()
24
+
25
+
26
+ def _initialize_db(self):
27
+ """
28
+ Initializes the SQLite database and creates LTM table
29
+ """
30
+ try:
31
+ with sqlite3.connect(self.db_path) as conn:
32
+ cursor = conn.cursor()
33
+ cursor.execute(
34
+ """
35
+ CREATE TABLE IF NOT EXISTS long_term_memories (
36
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
37
+ task_description TEXT,
38
+ metadata TEXT,
39
+ datetime TEXT,
40
+ score REAL
41
+ )
42
+ """
43
+ )
44
+
45
+ conn.commit()
46
+
47
+ except sqlite3.Error as e:
48
+ self._logger.log(
49
+ level="error",
50
+ message=f"MEMORY ERROR: An error occurred during database initialization: {str(e)}",
51
+ color="red",
52
+ )
53
+
54
+ def save(self, task_description: str, metadata: Dict[str, Any], datetime: str, score: int | float) -> None:
55
+ """
56
+ Saves data to the LTM table with error handling.
57
+ """
58
+ try:
59
+ with sqlite3.connect(self.db_path) as conn:
60
+ cursor = conn.cursor()
61
+ cursor.execute(
62
+ """
63
+ INSERT INTO long_term_memories (task_description, metadata, datetime, score)
64
+ VALUES (?, ?, ?, ?)
65
+ """,
66
+ (task_description, json.dumps(metadata), datetime, score),
67
+ )
68
+ conn.commit()
69
+ except sqlite3.Error as e:
70
+ self._logger.log(
71
+ level="error",
72
+ message=f"MEMORY ERROR: An error occurred while saving to LTM: {str(e)}",
73
+ color="red",
74
+ )
75
+
76
+
77
+ def load(self, task_description: str, latest_n: int) -> Optional[List[Dict[str, Any]]]:
78
+ """
79
+ Queries the LTM table by task description with error handling.
80
+ """
81
+ try:
82
+ with sqlite3.connect(self.db_path) as conn:
83
+ cursor = conn.cursor()
84
+ cursor.execute(
85
+ f"""
86
+ SELECT metadata, datetime, score
87
+ FROM long_term_memories
88
+ WHERE task_description = ?
89
+ ORDER BY datetime DESC, score ASC
90
+ LIMIT {latest_n}
91
+ """,
92
+ (task_description,),
93
+ )
94
+ rows = cursor.fetchall()
95
+ if rows:
96
+ return [
97
+ {
98
+ "metadata": json.loads(row[0]),
99
+ "datetime": row[1],
100
+ "score": row[2],
101
+ }
102
+ for row in rows
103
+ ]
104
+
105
+ except sqlite3.Error as e:
106
+ self._logger.log(
107
+ level="error",
108
+ message=f"MEMORY ERROR: An error occurred while querying LTM: {e}",
109
+ color="red",
110
+ )
111
+ return None
112
+
113
+
114
+ def reset(self) -> None:
115
+ """
116
+ Resets the LTM table with error handling.
117
+ """
118
+
119
+ try:
120
+ with sqlite3.connect(self.db_path) as conn:
121
+ cursor = conn.cursor()
122
+ cursor.execute("DELETE FROM long_term_memories")
123
+ conn.commit()
124
+
125
+ except sqlite3.Error as e:
126
+ self._logger.log(
127
+ level="error",
128
+ message=f"MEMORY ERROR: An error occurred while deleting all rows in LTM: {str(e)}",
129
+ color="red",
130
+ )
131
+ return None
@@ -0,0 +1,109 @@
1
+ import os
2
+ from typing import Any, Dict, List
3
+
4
+ from mem0 import MemoryClient
5
+
6
+ from versionhq.storage.base import Storage
7
+
8
+
9
+ class Mem0Storage(Storage):
10
+ """
11
+ Extends Storage to handle embedding and searching across entities using Mem0.
12
+ """
13
+
14
+ def __init__(self, type, agent=None, user_id=None):
15
+ """
16
+ Create a memory client using API keys and other config.
17
+ """
18
+
19
+ super().__init__()
20
+
21
+ if type not in ["user", "stm", "ltm", "entities"]:
22
+ raise ValueError("Invalid type for Mem0Storage. Must be 'user' or 'agent'.")
23
+
24
+ self.memory_type = type
25
+ self.agent= agent
26
+ self.memory_config = agent.memory_config
27
+
28
+ user_id = user_id if user_id else self._get_user_id()
29
+ if type == "user" and not user_id:
30
+ raise ValueError("User ID is required for user memory type")
31
+
32
+ config = self.memory_config.get("config", {})
33
+ mem0_api_key = os.environ.get("MEM0_API_KEY", config.get("api_key"))
34
+ mem0_org_id = config.get("org_id")
35
+ mem0_project_id = config.get("project_id")
36
+
37
+ if mem0_org_id and mem0_project_id:
38
+ self.memory = MemoryClient(api_key=mem0_api_key, org_id=mem0_org_id, project_id=mem0_project_id)
39
+ else:
40
+ self.memory = MemoryClient(api_key=mem0_api_key)
41
+
42
+
43
+ def _sanitize_role(self, role: str) -> str:
44
+ """
45
+ Sanitizes agent roles to ensure valid directory names.
46
+ """
47
+ return role.replace("\n", "").replace(" ", "_").replace("/", "_")
48
+
49
+
50
+ def save(self, value: Any, metadata: Dict[str, Any]) -> None:
51
+ user_id = self._get_user_id()
52
+ agent_name = self._get_agent_name()
53
+
54
+ if self.memory_type == "user":
55
+ self.memory.add(value, user_id=user_id, metadata={**metadata})
56
+
57
+ elif self.memory_type == "stm":
58
+ agent_name = self._get_agent_name()
59
+ self.memory.add(value, agent_id=agent_name, metadata={"type": "stm", **metadata})
60
+
61
+ elif self.memory_type == "ltm":
62
+ agent_name = self._get_agent_name()
63
+ self.memory.add(value, agent_id=agent_name, infer=False, metadata={"type": "ltm", **metadata})
64
+
65
+ elif self.memory_type == "entities":
66
+ entity_name = self._get_agent_name()
67
+ self.memory.add(value, user_id=entity_name, metadata={"type": "entity", **metadata})
68
+
69
+
70
+ def search(self, query: str, limit: int = 3, score_threshold: float = 0.35) -> List[Any]:
71
+ params = {"query": query, "limit": limit}
72
+
73
+ if self.memory_type == "user":
74
+ user_id = self._get_user_id()
75
+ params["user_id"] = user_id
76
+
77
+ elif self.memory_type == "stm":
78
+ agent_name = self._get_agent_name()
79
+ params["agent_id"] = agent_name
80
+ params["metadata"] = {"type": "stm"}
81
+
82
+ elif self.memory_type == "ltm":
83
+ agent_name = self._get_agent_name()
84
+ params["agent_id"] = agent_name
85
+ params["metadata"] = {"type": "ltm"}
86
+
87
+ elif self.memory_type == "entities":
88
+ agent_name = self._get_agent_name()
89
+ params["agent_id"] = agent_name
90
+ params["metadata"] = {"type": "entity"}
91
+
92
+ results = self.memory.search(**params)
93
+ return [r for r in results if r["score"] >= score_threshold]
94
+
95
+
96
+ def _get_user_id(self):
97
+ if self.memory_type == "user":
98
+ if hasattr(self, "memory_config") and self.memory_config is not None:
99
+ return self.memory_config.get("config", {}).get("user_id")
100
+ else:
101
+ return None
102
+ return None
103
+
104
+
105
+ def _get_agent_name(self):
106
+ agents = self.agents if self.agents else []
107
+ agents = [self._sanitize_role(agent.role) for agent in agents]
108
+ agents = "_".join(agents)
109
+ return agents
@@ -0,0 +1,231 @@
1
+ import contextlib
2
+ import io
3
+ import logging
4
+ import os
5
+ import shutil
6
+ import uuid
7
+ from abc import ABC, abstractmethod
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ from chromadb.api import ClientAPI
11
+
12
+ from versionhq.knowledge.embedding import EmbeddingConfigurator
13
+ from versionhq._utils.vars import MAX_FILE_NAME_LENGTH
14
+ from versionhq.storage.utils import fetch_db_storage_path
15
+
16
+
17
+ @contextlib.contextmanager
18
+ def suppress_logging(
19
+ logger_name="chromadb.segment.impl.vector.local_persistent_hnsw",
20
+ level=logging.ERROR,
21
+ ):
22
+ logger = logging.getLogger(logger_name)
23
+ original_level = logger.getEffectiveLevel()
24
+ logger.setLevel(level)
25
+ with (
26
+ contextlib.redirect_stdout(io.StringIO()),
27
+ contextlib.redirect_stderr(io.StringIO()),
28
+ contextlib.suppress(UserWarning),
29
+ ):
30
+ yield
31
+ logger.setLevel(original_level)
32
+
33
+
34
+ class BaseRAGStorage(ABC):
35
+ """
36
+ Base class for RAG-based Storage implementations.
37
+ """
38
+
39
+ app: Any | None = None
40
+
41
+ def __init__(
42
+ self,
43
+ type: str,
44
+ allow_reset: bool = True,
45
+ embedder_config: Optional[Any] = None,
46
+ agents: List[Any] = None,
47
+ ):
48
+ self.type = type
49
+ self.allow_reset = allow_reset
50
+ self.embedder_config = embedder_config
51
+ self.agents = agents
52
+
53
+ def _initialize_agents(self) -> str:
54
+ if self.agents:
55
+ return "_".join(
56
+ [self._sanitize_role(agent.role) for agent in self.agents]
57
+ )
58
+ return ""
59
+
60
+ @abstractmethod
61
+ def _sanitize_role(self, role: str) -> str:
62
+ """Sanitizes agent roles to ensure valid directory names."""
63
+ pass
64
+
65
+ @abstractmethod
66
+ def save(self, value: Any, metadata: Dict[str, Any]) -> None:
67
+ """Save a value with metadata to the storage."""
68
+ pass
69
+
70
+ @abstractmethod
71
+ def search(self, query: str, limit: int = 3, filter: Optional[dict] = None, score_threshold: float = 0.35) -> List[Any]:
72
+ """Search for entries in the storage."""
73
+ pass
74
+
75
+ @abstractmethod
76
+ def reset(self) -> None:
77
+ """Reset the storage."""
78
+ pass
79
+
80
+ @abstractmethod
81
+ def _generate_embedding(self, text: str, metadata: Optional[Dict[str, Any]] = None) -> Any:
82
+ """Generate an embedding for the given text and metadata."""
83
+ pass
84
+
85
+ @abstractmethod
86
+ def _initialize_app(self):
87
+ """Initialize the vector db."""
88
+ pass
89
+
90
+ def setup_config(self, config: Dict[str, Any]):
91
+ """Setup the config of the storage."""
92
+ pass
93
+
94
+ def initialize_client(self):
95
+ """Initialize the client of the storage. This should setup the app and the db collection"""
96
+ pass
97
+
98
+
99
+
100
+ class RAGStorage(BaseRAGStorage):
101
+ """
102
+ Extends Storage to handle embeddings for memory entries, improving
103
+ search efficiency.
104
+ """
105
+
106
+ app: ClientAPI | None = None
107
+
108
+ def __init__(self, type, allow_reset=True, embedder_config=None, agents=list(), path=None):
109
+ super().__init__(type, allow_reset, embedder_config, agents)
110
+ agents = agents
111
+ agents = [self._sanitize_role(agent.role) for agent in agents]
112
+ agents = "_".join(agents)
113
+
114
+ self.agents = agents
115
+ self.storage_file_name = self._build_storage_file_name(type, agents)
116
+ self.type = type
117
+ self.allow_reset = allow_reset
118
+ self.path = path
119
+ self._initialize_app()
120
+
121
+
122
+ def _set_embedder_config(self):
123
+ configurator = EmbeddingConfigurator()
124
+ self.embedder_config = configurator.configure_embedder(self.embedder_config)
125
+
126
+
127
+ def _initialize_app(self) -> None:
128
+ import chromadb
129
+ from chromadb.config import Settings
130
+
131
+ self._set_embedder_config()
132
+ chroma_client = chromadb.PersistentClient(
133
+ path=self.path if self.path else self.storage_file_name,
134
+ settings=Settings(allow_reset=self.allow_reset),
135
+ )
136
+ self.app = chroma_client
137
+
138
+ try:
139
+ self.collection = self.app.get_collection(name=self.type, embedding_function=self.embedder_config)
140
+ except Exception:
141
+ self.collection = self.app.create_collection(name=self.type, embedding_function=self.embedder_config)
142
+
143
+
144
+ def _sanitize_role(self, role: str) -> str:
145
+ """
146
+ Sanitizes agent roles to ensure valid directory names.
147
+ """
148
+ return role.replace("\n", "").replace(" ", "_").replace("/", "_")
149
+
150
+
151
+ def _build_storage_file_name(self, type: str, file_name: str) -> str:
152
+ """
153
+ Ensures file name does not exceed max allowed by OS
154
+ """
155
+ base_path = f"{fetch_db_storage_path()}/{type}"
156
+
157
+ if len(file_name) > MAX_FILE_NAME_LENGTH:
158
+ logging.warning(
159
+ f"Trimming file name from {len(file_name)} to {MAX_FILE_NAME_LENGTH} characters."
160
+ )
161
+ file_name = file_name[:MAX_FILE_NAME_LENGTH]
162
+
163
+ return f"{base_path}/{file_name}"
164
+
165
+
166
+ def save(self, value: Any, metadata: Dict[str, Any]) -> None:
167
+ if not hasattr(self, "app") or not hasattr(self, "collection"):
168
+ self._initialize_app()
169
+ try:
170
+ self._generate_embedding(value, metadata)
171
+ except Exception as e:
172
+ logging.error(f"Error during {self.type} save: {str(e)}")
173
+
174
+
175
+ def search(self, query: str, limit: int = 3, filter: Optional[dict] = None, score_threshold: float = 0.35) -> List[Any]:
176
+ if not hasattr(self, "app"):
177
+ self._initialize_app()
178
+
179
+ try:
180
+ with suppress_logging():
181
+ response = self.collection.query(query_texts=query, n_results=limit)
182
+
183
+ results = []
184
+ for i in range(len(response["ids"][0])):
185
+ result = {
186
+ "id": response["ids"][0][i],
187
+ "metadata": response["metadatas"][0][i],
188
+ "context": response["documents"][0][i],
189
+ "score": response["distances"][0][i],
190
+ }
191
+ if result["score"] >= score_threshold:
192
+ results.append(result)
193
+
194
+ return results
195
+ except Exception as e:
196
+ logging.error(f"Error during {self.type} search: {str(e)}")
197
+ return []
198
+
199
+
200
+ def _generate_embedding(self, text: str, metadata: Dict[str, Any]) -> None:
201
+ if not hasattr(self, "app") or not hasattr(self, "collection"):
202
+ self._initialize_app()
203
+
204
+ self.collection.add(
205
+ documents=[text],
206
+ metadatas=[metadata or {}],
207
+ ids=[str(uuid.uuid4())],
208
+ )
209
+
210
+
211
+ def reset(self) -> None:
212
+ try:
213
+ if self.app:
214
+ self.app.reset()
215
+ shutil.rmtree(f"{fetch_db_storage_path()}/{self.type}")
216
+ self.app = None
217
+ self.collection = None
218
+ except Exception as e:
219
+ if "attempt to write a readonly database" in str(e):
220
+ pass
221
+ else:
222
+ raise Exception(f"An error occurred while resetting the {self.type} memory: {e}")
223
+
224
+ def _create_default_embedding_function(self):
225
+ from chromadb.utils.embedding_functions.openai_embedding_function import (
226
+ OpenAIEmbeddingFunction,
227
+ )
228
+
229
+ return OpenAIEmbeddingFunction(
230
+ api_key=os.getenv("OPENAI_API_KEY"), model_name="text-embedding-3-small"
231
+ )
@@ -1,21 +1,10 @@
1
- import appdirs
2
- import os
3
1
  import json
4
2
  import sqlite3
5
3
  import datetime
6
4
  from typing import Any, Dict, List, Optional
7
- from dotenv import load_dotenv
8
- from pathlib import Path
9
5
 
10
6
  from versionhq._utils.logger import Logger
11
-
12
- load_dotenv(override=True)
13
-
14
- def fetch_db_storage_path():
15
- directory_name = Path.cwd().name
16
- data_dir = Path(appdirs.user_data_dir(appname=directory_name, appauthor="Version IO Sdn Bhd.", version=None, roaming=False))
17
- data_dir.mkdir(parents=True, exist_ok=True)
18
- return data_dir
7
+ from versionhq.storage.utils import fetch_db_storage_path
19
8
 
20
9
  storage_path = fetch_db_storage_path()
21
10
  default_db_name = "task_outputs"
@@ -37,25 +26,25 @@ class TaskOutputSQLiteStorage:
37
26
  Initializes the SQLite database and creates LTM table.
38
27
  """
39
28
 
40
- # try:
41
- with sqlite3.connect(self.db_path) as conn:
42
- cursor = conn.cursor()
43
- cursor.execute(
29
+ try:
30
+ with sqlite3.connect(self.db_path) as conn:
31
+ cursor = conn.cursor()
32
+ cursor.execute(
33
+ """
34
+ CREATE TABLE IF NOT EXISTS task_outputs (
35
+ task_id TEXT PRIMARY KEY,
36
+ output JSON,
37
+ task_index INTEGER,
38
+ inputs JSON,
39
+ was_replayed BOOLEAN,
40
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
41
+ )
44
42
  """
45
- CREATE TABLE IF NOT EXISTS task_outputs (
46
- task_id TEXT PRIMARY KEY,
47
- output JSON,
48
- task_index INTEGER,
49
- inputs JSON,
50
- was_replayed BOOLEAN,
51
- timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
52
43
  )
53
- """
54
- )
55
- conn.commit()
44
+ conn.commit()
56
45
 
57
- # except sqlite3.Error as e:
58
- # self._logger.log(level="error", message=f"DATABASE INITIALIZATION ERROR: {e}", color="red")
46
+ except sqlite3.Error as e:
47
+ self._logger.log(level="error", message=f"SQL database initialization failed: {str(e)}", color="red")
59
48
 
60
49
 
61
50
  def add(self, task, output: Dict[str, Any], task_index: int, was_replayed: bool = False, inputs: Dict[str, Any] = {}):
@@ -91,7 +80,7 @@ class TaskOutputSQLiteStorage:
91
80
 
92
81
  if cursor.rowcount == 0:
93
82
  self._logger.log(
94
- level="info", message=f"No row found with task_index {task_index}. No update performed.", color="yellow",
83
+ level="warning", message=f"No row found with task_index {task_index}. No update performed.", color="yellow",
95
84
  )
96
85
 
97
86
  except sqlite3.Error as e:
@@ -0,0 +1,26 @@
1
+ import appdirs
2
+ import os
3
+ from dotenv import load_dotenv
4
+ from pathlib import Path
5
+
6
+ load_dotenv(override=True)
7
+
8
+ def fetch_db_storage_path() -> str:
9
+ directory_name = get_project_directory_name()
10
+ data_dir = Path(appdirs.user_data_dir(appname=directory_name, appauthor="Version IO Sdn Bhd", version=None, roaming=False))
11
+ data_dir.mkdir(parents=True, exist_ok=True)
12
+ return str(data_dir)
13
+
14
+
15
+ def get_project_directory_name() -> str:
16
+ """
17
+ Returns the current project directory name
18
+ """
19
+ project_directory_name = os.environ.get("STORAGE_DIR")
20
+
21
+ if project_directory_name:
22
+ return project_directory_name
23
+ else:
24
+ cwd = Path.cwd()
25
+ project_directory_name = cwd.name
26
+ return project_directory_name
@@ -0,0 +1,5 @@
1
+ EVALUATE="""Assess the accuracy and quality of the following task output to the task described below. Score based on the criterion (0-1, 0=worst, 1=best) and suggest improvements. Vary scores; don't assign identical values. Store criteria in the "criteria" field.
2
+ Task: {task_description}
3
+ Task Output: {task_output}
4
+ Evaluation criteria: {eval_criteria}
5
+ """
@@ -0,0 +1,122 @@
1
+ from typing import List, Optional, Dict, Any
2
+ from typing_extensions import Self
3
+
4
+ from pydantic import BaseModel, Field, InstanceOf, model_validator
5
+
6
+ """
7
+ Evaluate task output from accuracy, token consumption, latency perspectives, and mark the score from 0 to 1.
8
+ """
9
+
10
+
11
+
12
+
13
+ class ScoreFormat:
14
+ def __init__(self, rate: float | int = 0, weight: int = 1):
15
+ self.rate = rate
16
+ self.weight = weight
17
+ self.aggregate = rate * weight
18
+
19
+
20
+ class Score:
21
+ """
22
+ Evaluate the score on 0 (no performance) to 1 scale.
23
+ `rate`: Any float from 0.0 to 1.0 given by an agent.
24
+ `weight`: Importance of each factor to the aggregated score.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ brand_tone: ScoreFormat = ScoreFormat(0, 0),
30
+ audience: ScoreFormat = ScoreFormat(0, 0),
31
+ track_record: ScoreFormat = ScoreFormat(0, 0),
32
+ config: Optional[Dict[str, ScoreFormat]] = None
33
+ ):
34
+ self.brand_tone = brand_tone
35
+ self.audience = audience
36
+ self.track_record = track_record
37
+ self.config = config
38
+
39
+ if self.config:
40
+ for k, v in self.config.items():
41
+ if isinstance(v, ScoreFormat):
42
+ setattr(self, k, v)
43
+
44
+
45
+ def result(self) -> int:
46
+ aggregate_score, denominator = 0, 0
47
+
48
+ for k, v in self.__dict__.items():
49
+ aggregate_score += v.aggregate
50
+ denominator += v.weight
51
+
52
+ if denominator == 0:
53
+ return 0
54
+
55
+ return round(aggregate_score / denominator, 2)
56
+
57
+
58
+ class EvaluationItem(BaseModel):
59
+ """
60
+ A class to store evaluation and suggestion by the given criteria such as accuracy.
61
+ """
62
+ criteria: str
63
+ suggestion: str
64
+ score: int | float
65
+
66
+ def _convert_score_to_score_format(self, weight: int = 1) -> ScoreFormat | None:
67
+ if self.score and isinstance(self.score, (int, float)):
68
+ return ScoreFormat(rate=self.score, weight=weight)
69
+
70
+ else: return None
71
+
72
+
73
+
74
+ class Evaluation(BaseModel):
75
+ # expected_outcome: Optional[str] = Field(default=None, description="human input on expected outcome")
76
+ items: List[EvaluationItem] = []
77
+ latency: int = Field(default=None, description="seconds")
78
+ tokens: int = Field(default=None, description="tokens consumed")
79
+ responsible_agent: Any = Field(default=None, description="store agent instance that evaluates the outcome")
80
+
81
+ @model_validator(mode="after")
82
+ def set_up_responsible_agent(self) -> Self:
83
+ from versionhq.agent.default_agents import task_evaluator
84
+ self.responsible_agent = task_evaluator
85
+ return self
86
+
87
+
88
+ @property
89
+ def aggregate_score(self) -> float:
90
+ """
91
+ Calcurate aggregate score from evaluation items.
92
+ """
93
+ if not self.items:
94
+ return 0
95
+
96
+ aggregate_score = 0
97
+ denominator = 0
98
+
99
+ for item in self.items:
100
+ score_format = item._convert_score_to_score_format()
101
+ aggregate_score += score_format.aggregate if score_format else 0
102
+ denominator += score_format.weight if score_format else 0
103
+
104
+ if denominator == 0:
105
+ return 0
106
+
107
+ return round(aggregate_score / denominator, 2)
108
+
109
+
110
+ @property
111
+ def suggestion_summary(self) -> str | None:
112
+ """
113
+ Return a summary of the suggestions
114
+ """
115
+ if not self.items:
116
+ return None
117
+
118
+ summary = ""
119
+ for item in self.items:
120
+ summary += f"{item.suggestion}, "
121
+
122
+ return summary