modaic 0.1.2__py3-none-any.whl → 0.2.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.

Potentially problematic release.


This version of modaic might be problematic. Click here for more details.

modaic/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- from .auto_agent import AutoAgent, AutoConfig, AutoRetriever
1
+ from .auto import AutoAgent, AutoConfig, AutoRetriever
2
2
  from .indexing import Embedder
3
3
  from .observability import Trackable, configure, track, track_modaic_obj
4
4
  from .precompiled import Indexer, PrecompiledAgent, PrecompiledConfig, Retriever
@@ -22,4 +22,16 @@ __all__ = [
22
22
  "Prop",
23
23
  "Value",
24
24
  "parse_modaic_filter",
25
+ "Condition",
25
26
  ]
27
+ _configured = False
28
+
29
+
30
+ def _auto_configure():
31
+ global _configured
32
+ if not _configured:
33
+ configure()
34
+ _configured = True
35
+
36
+
37
+ _auto_configure()
@@ -4,24 +4,38 @@ import os
4
4
  import sys
5
5
  from functools import lru_cache
6
6
  from pathlib import Path
7
- from typing import Literal, Optional, Type
7
+ from typing import Callable, Literal, Optional, Type, TypedDict
8
8
 
9
- from .hub import load_repo
9
+ from .hub import AGENTS_CACHE, load_repo
10
10
  from .precompiled import PrecompiledAgent, PrecompiledConfig, Retriever, is_local_path
11
11
 
12
12
  MODAIC_TOKEN = os.getenv("MODAIC_TOKEN")
13
13
 
14
14
 
15
- _REGISTRY = {} # maps model_type string -> (ConfigCls, ModelCls)
15
+ class RegisteredRepo(TypedDict, total=False):
16
+ AutoConfig: Type[PrecompiledConfig]
17
+ AutoAgent: Type[PrecompiledAgent]
18
+ AutoRetriever: Type[Retriever]
16
19
 
17
20
 
18
- def register(model_type: str, config_cls: Type[PrecompiledConfig], model_cls: Type[PrecompiledAgent]):
19
- _REGISTRY[model_type] = (config_cls, model_cls)
21
+ _REGISTRY: dict[str, RegisteredRepo] = {}
20
22
 
21
23
 
24
+ def register(
25
+ name: str,
26
+ auto_type: Literal["AutoConfig", "AutoAgent", "AutoRetriever"],
27
+ cls: Type[PrecompiledConfig | PrecompiledAgent | Retriever],
28
+ ):
29
+ if name in _REGISTRY:
30
+ _REGISTRY[name][auto_type] = cls
31
+ else:
32
+ _REGISTRY[name] = {auto_type: cls}
33
+
34
+
35
+ # TODO: Cleanup code still using parent_mdoule
22
36
  @lru_cache
23
37
  def _load_dynamic_class(
24
- repo_dir: str, class_path: str, parent_module: Optional[str] = None
38
+ repo_dir: Path, class_path: str, hub_path: str = None
25
39
  ) -> Type[PrecompiledConfig | PrecompiledAgent | Retriever]:
26
40
  """
27
41
  Load a class from a given repository directory and fully qualified class path.
@@ -29,27 +43,24 @@ def _load_dynamic_class(
29
43
  Args:
30
44
  repo_dir: Absolute path to a local repository directory containing the code.
31
45
  class_path: Dotted path to the target class (e.g., "pkg.module.Class").
32
- parent_module: Optional dotted module prefix (e.g., "swagginty.TableRAG"). If provided,
33
- class_path is treated as relative to this module and only the agents cache
34
- root is added to sys.path.
46
+ hub_path: The path to the repo on modaic hub (if its a hub repo) *Must be specified if its a hub repo*
35
47
 
36
48
  Returns:
37
49
  The resolved class object.
38
50
  """
39
-
40
- repo_path = Path(repo_dir)
41
-
42
- repo_dir_str = str(repo_path)
43
- print(f"repo_dir_str: {repo_dir_str}")
44
- print(f"sys.path: {sys.path}")
45
- if repo_dir_str not in sys.path:
46
- # print(f"Inserting {repo_dir_str} into sys.path")
47
- sys.path.insert(0, repo_dir_str)
48
- full_path = (
49
- f"{parent_module}.{class_path}"
50
- if parent_module and not class_path.startswith(parent_module + ".")
51
- else class_path
52
- )
51
+ if hub_path is None:
52
+ # Local folder case
53
+ repo_dir_str = str(repo_dir)
54
+ if repo_dir_str not in sys.path:
55
+ sys.path.insert(0, repo_dir_str)
56
+ full_path = f"{class_path}"
57
+ else:
58
+ # loaded hub repo case
59
+ agents_cache_str = str(AGENTS_CACHE)
60
+ if agents_cache_str not in sys.path:
61
+ sys.path.insert(0, agents_cache_str)
62
+ parent_module = hub_path.replace("/", ".")
63
+ full_path = f"{parent_module}.{class_path}"
53
64
 
54
65
  module_name, _, attr = full_path.rpartition(".")
55
66
  module = importlib.import_module(module_name)
@@ -62,27 +73,31 @@ class AutoConfig:
62
73
  """
63
74
 
64
75
  @staticmethod
65
- def from_precompiled(repo_path: str, *, parent_module: Optional[str] = None, **kwargs) -> PrecompiledConfig:
76
+ def from_precompiled(repo_path: str, **kwargs) -> PrecompiledConfig:
77
+ local = is_local_path(repo_path)
78
+ repo_dir = load_repo(repo_path, local)
79
+ return AutoConfig._from_precompiled(repo_dir, hub_path=repo_path if not local else None, **kwargs)
80
+
81
+ @staticmethod
82
+ def _from_precompiled(repo_dir: Path, hub_path: str = None, **kwargs) -> PrecompiledConfig:
66
83
  """
67
84
  Load a config for an agent or retriever from a precompiled repo.
68
85
 
69
86
  Args:
70
- repo_path: Hub path ("user/repo") or a local directory.
71
- parent_module: Optional dotted module prefix (e.g., "swagginty.TableRAG") to use to import classes from repo_path. If provided, overides default parent_module behavior.
87
+ repo_dir: The path to the repo directory. the loaded local repository directory.
88
+ hub_path: The path to the repo on modaic hub (if its a hub repo) *Must be specified if its a hub repo*
72
89
 
73
90
  Returns:
74
91
  A config object constructed via the resolved config class.
75
92
  """
76
- local = is_local_path(repo_path)
77
- repo_dir = load_repo(repo_path, local)
78
93
 
79
94
  cfg_path = repo_dir / "config.json"
80
95
  if not cfg_path.exists():
81
- raise FileNotFoundError(f"Failed to load AutoConfig, config.json not found in {repo_path}")
96
+ raise FileNotFoundError(f"Failed to load AutoConfig, config.json not found in {hub_path or str(repo_dir)}")
82
97
  with open(cfg_path, "r") as fp:
83
98
  cfg = json.load(fp)
84
99
 
85
- ConfigClass = _load_auto_class(repo_path, repo_dir, "AutoConfig", parent_module=parent_module) # noqa: N806
100
+ ConfigClass = _load_auto_class(repo_dir, "AutoConfig", hub_path=hub_path) # noqa: N806
86
101
  return ConfigClass(**{**cfg, **kwargs})
87
102
 
88
103
 
@@ -96,7 +111,6 @@ class AutoAgent:
96
111
  repo_path: str,
97
112
  *,
98
113
  config_options: Optional[dict] = None,
99
- parent_module: Optional[str] = None,
100
114
  project: Optional[str] = None,
101
115
  **kw,
102
116
  ) -> PrecompiledAgent:
@@ -105,23 +119,25 @@ class AutoAgent:
105
119
 
106
120
  Args:
107
121
  repo_path: Hub path ("user/repo") or local directory.
108
- parent_module: Optional dotted module prefix (e.g., "swagginty.TableRAG") to use to import classes from repo_path. If provided, overides default parent_module behavior.
109
122
  project: Optional project name. If not provided and repo_path is a hub path, defaults to the repo name.
110
123
  **kw: Additional keyword arguments forwarded to the Agent constructor.
111
124
 
112
125
  Returns:
113
126
  An instantiated Agent subclass.
114
127
  """
128
+ # TODO: fast lookups via registry
115
129
  local = is_local_path(repo_path)
116
130
  repo_dir = load_repo(repo_path, local)
131
+ hub_path = repo_path if not local else None
117
132
 
118
133
  if config_options is None:
119
134
  config_options = {}
120
135
 
121
- cfg = AutoConfig.from_precompiled(repo_dir, local=True, parent_module=parent_module, **config_options)
122
- AgentClass = _load_auto_class(repo_path, repo_dir, "AutoAgent", parent_module=parent_module) # noqa: N806
136
+ cfg = AutoConfig._from_precompiled(repo_dir, hub_path=hub_path, **config_options)
137
+ AgentClass = _load_auto_class(repo_dir, "AutoAgent", hub_path=hub_path) # noqa: N806
123
138
 
124
139
  # automatically configure repo and project from repo_path if not provided
140
+ # TODO: redundant checks in if statement. Investigate removing.
125
141
  if not local and "/" in repo_path and not repo_path.startswith("/"):
126
142
  parts = repo_path.split("/")
127
143
  if len(parts) >= 2:
@@ -146,7 +162,6 @@ class AutoRetriever:
146
162
  repo_path: str,
147
163
  *,
148
164
  config_options: Optional[dict] = None,
149
- parent_module: Optional[str] = None,
150
165
  project: Optional[str] = None,
151
166
  **kw,
152
167
  ) -> Retriever:
@@ -155,7 +170,6 @@ class AutoRetriever:
155
170
 
156
171
  Args:
157
172
  repo_path: hub path ("user/repo"), or local directory.
158
- parent_module: Optional dotted module prefix (e.g., "swagginty.TableRAG") to use to import classes from repo_path. If provided, overides default parent_module behavior.
159
173
  project: Optional project name. If not provided and repo_path is a hub path, defaults to the repo name.
160
174
  **kw: Additional keyword arguments forwarded to the Retriever constructor.
161
175
 
@@ -164,14 +178,16 @@ class AutoRetriever:
164
178
  """
165
179
  local = is_local_path(repo_path)
166
180
  repo_dir = load_repo(repo_path, local)
181
+ hub_path = repo_path if not local else None
167
182
 
168
183
  if config_options is None:
169
184
  config_options = {}
170
185
 
171
- cfg = AutoConfig.from_precompiled(repo_dir, local=True, parent_module=parent_module, **config_options)
172
- RetrieverClass = _load_auto_class(repo_path, repo_dir, "AutoRetriever", parent_module=parent_module) # noqa: N806
186
+ cfg = AutoConfig._from_precompiled(repo_dir, hub_path=hub_path, **config_options)
187
+ RetrieverClass = _load_auto_class(repo_dir, "AutoRetriever", hub_path=hub_path) # noqa: N806
173
188
 
174
189
  # automatically configure repo and project from repo_path if not provided
190
+ # TODO: redundant checks in if statement. Investigate removing.
175
191
  if not local and "/" in repo_path and not repo_path.startswith("/"):
176
192
  parts = repo_path.split("/")
177
193
  if len(parts) >= 2:
@@ -186,27 +202,25 @@ class AutoRetriever:
186
202
 
187
203
 
188
204
  def _load_auto_class(
189
- repo_path: str,
190
205
  repo_dir: Path,
191
206
  auto_name: Literal["AutoConfig", "AutoAgent", "AutoRetriever"],
192
- parent_module: Optional[str] = None,
207
+ hub_path: str = None,
193
208
  ) -> Type[PrecompiledConfig | PrecompiledAgent | Retriever]:
194
209
  """
195
210
  Load a class from the auto_classes.json file.
196
211
 
197
212
  Args:
198
- repo_path: The path to the repo. (local or hub path)
199
213
  repo_dir: The path to the repo directory. the loaded local repository directory.
200
214
  auto_name: The name of the auto class to load. (AutoConfig, AutoAgent, AutoRetriever)
201
- parent_module: The parent module to use to import the class.
215
+ hub_path: The path to the repo on modaic hub (if its a hub repo) *Must be specified if its a hub repo*
202
216
  """
203
217
  # determine if the repo was loaded from local or hub
204
- local = is_local_path(repo_path)
218
+ local = hub_path is None
205
219
  auto_classes_path = repo_dir / "auto_classes.json"
206
220
 
207
221
  if not auto_classes_path.exists():
208
222
  raise FileNotFoundError(
209
- f"Failed to load {auto_name}, auto_classes.json not found in {repo_path}, if this is your repo, make sure you push_to_hub() with `with_code=True`"
223
+ f"Failed to load {auto_name}, auto_classes.json not found in {hub_path or str(repo_dir)}, if this is your repo, make sure you push_to_hub() with `with_code=True`"
210
224
  )
211
225
 
212
226
  with open(auto_classes_path, "r") as fp:
@@ -214,15 +228,33 @@ def _load_auto_class(
214
228
 
215
229
  if not (auto_class_path := auto_classes.get(auto_name)):
216
230
  raise KeyError(
217
- f"{auto_name} not found in {repo_path}/auto_classes.json. Please check that the auto_classes.json file is correct."
231
+ f"{auto_name} not found in {hub_path or str(repo_dir)}/auto_classes.json. Please check that the auto_classes.json file is correct."
218
232
  ) from None
219
233
 
220
- if auto_class_path in _REGISTRY:
221
- _, LoadedClass = _REGISTRY[auto_class_path] # noqa: N806
222
- else:
223
- if parent_module is None and not local:
224
- parent_module = str(repo_path).replace("/", ".")
225
-
226
- repo_dir = repo_dir.parent.parent if not local else repo_dir
227
- LoadedClass = _load_dynamic_class(repo_dir, auto_class_path, parent_module=parent_module) # noqa: N806
234
+ repo_dir = repo_dir.parent.parent if not local else repo_dir
235
+ LoadedClass = _load_dynamic_class(repo_dir, auto_class_path, hub_path=hub_path) # noqa: N806
228
236
  return LoadedClass
237
+
238
+
239
+ def builtin_agent(name: str) -> Callable[[Type], Type]:
240
+ def _wrap(cls: Type) -> Type:
241
+ register(name, "AutoAgent", cls)
242
+ return cls
243
+
244
+ return _wrap
245
+
246
+
247
+ def builtin_indexer(name: str) -> Callable[[Type], Type]:
248
+ def _wrap(cls: Type) -> Type:
249
+ register(name, "AutoRetriever", cls)
250
+ return cls
251
+
252
+ return _wrap
253
+
254
+
255
+ def builtin_config(name: str) -> Callable[[Type], Type]:
256
+ def _wrap(cls: Type) -> Type:
257
+ register(name, "AutoConfig", cls)
258
+ return cls
259
+
260
+ return _wrap
@@ -12,7 +12,7 @@ from .table import (
12
12
  Table,
13
13
  TableFile,
14
14
  )
15
- from .text import Text
15
+ from .text import Text, TextFile
16
16
 
17
17
  __all__ = [
18
18
  "MultiTabbedTable",
@@ -31,4 +31,5 @@ __all__ = [
31
31
  "Prop",
32
32
  "HydratedAttr",
33
33
  "requires_hydration",
34
+ "TextFile",
34
35
  ]
modaic/context/base.py CHANGED
@@ -22,7 +22,7 @@ from pydantic._internal._model_construction import ModelMetaclass
22
22
  from pydantic.fields import ModelPrivateAttr
23
23
  from pydantic.main import IncEx
24
24
  from pydantic.v1 import Field as V1Field
25
- from pydantic_core import CoreSchema, SchemaSerializer
25
+ from pydantic_core import SchemaSerializer
26
26
 
27
27
  from ..query_language import Prop
28
28
  from ..storage.file_store import FileStore
modaic/context/table.py CHANGED
@@ -38,7 +38,7 @@ class BaseTable(Context, ABC):
38
38
  """
39
39
  Return up to 3 distinct sample values from the given column.
40
40
 
41
- Picks at most three unique, non-null, short (<64 chars) values from
41
+ Picks at most three unique, non-null, short (&lt;64 chars) values from
42
42
  the column, favoring speed by sampling after de-duplicating values.
43
43
 
44
44
  Args:
modaic/context/text.py CHANGED
@@ -47,7 +47,7 @@ class TextFile(Context):
47
47
  file_type: Literal["txt"] = "txt"
48
48
 
49
49
  def hydrate(self, file_store: FileStore) -> None:
50
- file = file_store.get(self.file_ref)
50
+ file = file_store.get(self.file_ref).file
51
51
  if isinstance(file, Path):
52
52
  file = file.read_text()
53
53
  else:
@@ -91,4 +91,4 @@ class TextFile(Context):
91
91
  chunks.append(Text(text=chunk))
92
92
  return chunks
93
93
 
94
- self.apply_to_chunks(chunk_text_fn)
94
+ self.chunk_with(chunk_text_fn)
@@ -12,12 +12,13 @@ from typing import (
12
12
  Type,
13
13
  )
14
14
 
15
- from dotenv import load_dotenv
15
+ from dotenv import find_dotenv, load_dotenv
16
16
 
17
17
  from ..context.base import Context, Relation
18
18
  from ..observability import Trackable, track_modaic_obj
19
19
 
20
- load_dotenv()
20
+ env_file = find_dotenv(usecwd=True)
21
+ load_dotenv(env_file)
21
22
 
22
23
 
23
24
  if TYPE_CHECKING:
modaic/datasets.py ADDED
@@ -0,0 +1,22 @@
1
+ import dspy
2
+
3
+
4
+ class Dataset:
5
+ def __init__(self, data: list):
6
+ self.data = data
7
+
8
+ def to_dspy(self) -> list:
9
+ return dspy.Dataset(self.data)
10
+
11
+ @classmethod
12
+ def from_csv(cls, file_path: str) -> "Dataset":
13
+ with open(file_path, "r") as file:
14
+ data = file.read()
15
+ return cls(data)
16
+
17
+ @classmethod
18
+ def from_hub(cls, dataset_name: str) -> "Dataset":
19
+ from datasets import load_dataset
20
+
21
+ data = load_dataset(dataset_name)
22
+ return cls(data)
modaic/hub.py CHANGED
@@ -1,15 +1,17 @@
1
1
  import os
2
+ import shutil
2
3
  from pathlib import Path
3
4
  from typing import Any, Dict, Optional
4
5
 
5
6
  import git
6
7
  import requests
7
- from dotenv import load_dotenv
8
+ from dotenv import find_dotenv, load_dotenv
8
9
 
9
10
  from .exceptions import AuthenticationError, RepositoryExistsError, RepositoryNotFoundError
10
11
  from .utils import compute_cache_dir
11
12
 
12
- load_dotenv()
13
+ env_file = find_dotenv(usecwd=True)
14
+ load_dotenv(env_file)
13
15
 
14
16
  MODAIC_TOKEN = os.getenv("MODAIC_TOKEN")
15
17
  MODAIC_GIT_URL = os.getenv("MODAIC_GIT_URL", "git.modaic.dev").replace("https://", "").rstrip("/")
@@ -80,6 +82,7 @@ def create_remote_repo(repo_path: str, access_token: str, exist_ok: bool = False
80
82
  raise Exception(f"Request failed: {str(e)}") from e
81
83
 
82
84
 
85
+ # FIXME: make faster. Currently takes ~9 seconds
83
86
  def push_folder_to_hub(
84
87
  folder: str,
85
88
  repo_path: str,
@@ -123,10 +126,11 @@ def push_folder_to_hub(
123
126
  "Modaic fast paths not yet implemented. Please load agents with 'user/repo' or 'org/repo' format"
124
127
  )
125
128
  assert repo_path.count("/") <= 1, f"Extra '/' in repo_path: {repo_path}"
126
-
129
+ # TODO: try pushing first and on error create the repo. create_remote_repo currently takes ~1.5 seconds to run
127
130
  create_remote_repo(repo_path, access_token, exist_ok=True)
128
131
  username = get_user_info(access_token)["login"]
129
132
 
133
+ # FIXME: takes 6 seconds
130
134
  try:
131
135
  # 1) If local folder is not a git repository, initialize it.
132
136
  local_repo = git.Repo.init(folder)
@@ -205,6 +209,7 @@ def get_repo_payload(repo_name: str) -> Dict[str, Any]:
205
209
  return payload
206
210
 
207
211
 
212
+ # TODO: add persistent filesystem based cache mapping access_token to user_info. Currently takes ~1 second
208
213
  def get_user_info(access_token: str) -> Dict[str, Any]:
209
214
  """
210
215
  Returns the user info for the given access token.
@@ -214,12 +219,14 @@ def get_user_info(access_token: str) -> Dict[str, Any]:
214
219
  access_token: The access token to get the user info for.
215
220
 
216
221
  Returns:
222
+ ```python
217
223
  {
218
224
  "login": str,
219
225
  "email": str,
220
226
  "avatar_url": str,
221
227
  "name": str,
222
228
  }
229
+ ```
223
230
  """
224
231
  global user_info
225
232
  if user_info:
@@ -243,6 +250,7 @@ def get_user_info(access_token: str) -> Dict[str, Any]:
243
250
  return user_info
244
251
 
245
252
 
253
+ # TODO:
246
254
  def git_snapshot(
247
255
  repo_path: str,
248
256
  *,
@@ -265,7 +273,6 @@ def git_snapshot(
265
273
  elif access_token is None:
266
274
  raise ValueError("Access token is required")
267
275
 
268
- # If a local folder path is provided, just return it
269
276
  repo_dir = Path(AGENTS_CACHE) / repo_path
270
277
  username = get_user_info(access_token)["login"]
271
278
  try:
@@ -291,10 +298,26 @@ def git_snapshot(
291
298
  repo.git.reset("--hard", f"origin/{target}")
292
299
  return repo_dir
293
300
  except Exception as e:
294
- repo_dir.rmdir()
301
+ shutil.rmtree(repo_dir)
295
302
  raise e
296
303
 
297
304
 
305
+ def _move_to_commit_sha_folder(repo: git.Repo) -> git.Repo:
306
+ """
307
+ Moves the repo to a new path based on the commit SHA. (Unused for now)
308
+ Args:
309
+ repo: The git.Repo object.
310
+
311
+ Returns:
312
+ The new git.Repo object.
313
+ """
314
+ commit = repo.head.commit
315
+ repo_dir = Path(repo.working_dir)
316
+ new_path = repo_dir / commit.hexsha
317
+ repo_dir.rename(new_path)
318
+ return git.Repo(new_path)
319
+
320
+
298
321
  def load_repo(repo_path: str, is_local: bool = False) -> Path:
299
322
  if is_local:
300
323
  path = Path(repo_path)
modaic/indexing.py CHANGED
@@ -77,7 +77,7 @@ class PineconeReranker(Reranker):
77
77
  try:
78
78
  from pinecone import Pinecone
79
79
  except ImportError:
80
- raise ImportError("Pinecone is not installed. Please install it with `uv add pinecone`")
80
+ raise ImportError("Pinecone is not installed. Please install it with `uv add pinecone`") from None
81
81
 
82
82
  if api_key is None:
83
83
  self.pinecone = Pinecone(os.getenv("PINECONE_API_KEY"))
modaic/module_utils.py CHANGED
@@ -16,6 +16,7 @@ from .utils import compute_cache_dir
16
16
  MODAIC_CACHE = compute_cache_dir()
17
17
  AGENTS_CACHE = Path(MODAIC_CACHE) / "agents"
18
18
  EDITABLE_MODE = os.getenv("EDITABLE_MODE", "false").lower() == "true"
19
+ TEMP_DIR = Path(MODAIC_CACHE) / "temp"
19
20
 
20
21
 
21
22
  def is_builtin(module_name: str) -> bool:
@@ -76,6 +77,7 @@ def is_builtin_or_frozen(mod: ModuleType) -> bool:
76
77
  return (name in sys.builtin_module_names) or (origin in ("built-in", "frozen"))
77
78
 
78
79
 
80
+ # FIXME: make faster. Currently takes ~.70 seconds
79
81
  def get_internal_imports() -> Dict[str, ModuleType]:
80
82
  """Return only internal modules currently loaded in sys.modules.
81
83
 
@@ -107,6 +109,10 @@ def get_internal_imports() -> Dict[str, ModuleType]:
107
109
  if is_builtin_or_frozen(module):
108
110
  continue
109
111
 
112
+ # edge case: local modaic package
113
+ if name == "modaic" or "modaic." in name:
114
+ continue
115
+
110
116
  module_file = getattr(module, "__file__", None)
111
117
  if not module_file:
112
118
  continue
@@ -189,8 +195,11 @@ def is_external_package(path: Path) -> bool:
189
195
 
190
196
  def init_agent_repo(repo_path: str, with_code: bool = True) -> Path:
191
197
  """Create a local repository staging directory for agent modules and files, excluding ignored files and folders."""
192
- repo_dir = Path(AGENTS_CACHE) / repo_path
193
- repo_dir.mkdir(parents=True, exist_ok=True)
198
+ repo_dir = TEMP_DIR / repo_path
199
+ shutil.rmtree(repo_dir, ignore_errors=True)
200
+ repo_dir.mkdir(parents=True, exist_ok=False)
201
+
202
+ project_root = resolve_project_root()
194
203
 
195
204
  internal_imports = get_internal_imports()
196
205
  ignored_paths = get_ignored_files()
@@ -207,12 +216,12 @@ def init_agent_repo(repo_path: str, with_code: bool = True) -> Path:
207
216
  if not with_code:
208
217
  return repo_dir
209
218
 
210
- for module_name, module in internal_imports.items():
211
- module_file = getattr(module, "__file__", None)
219
+ for _, module in internal_imports.items():
220
+ module_file = Path(getattr(module, "__file__", None))
212
221
  if not module_file:
213
222
  continue
214
223
  try:
215
- src_path = Path(module_file).resolve()
224
+ src_path = module_file.resolve()
216
225
  except OSError:
217
226
  continue
218
227
  if src_path.suffix != ".py":
@@ -223,25 +232,27 @@ def init_agent_repo(repo_path: str, with_code: bool = True) -> Path:
223
232
  continue
224
233
  seen_files.add(src_path)
225
234
 
226
- # Split modul_name to get the relative path
227
- name_parts = module_name.split(".")
228
- if src_path.name == "__init__.py":
229
- copy_module_layout(repo_dir, name_parts)
230
- dest_path = repo_dir.joinpath(*name_parts) / "__init__.py"
231
- else:
232
- if len(name_parts) > 1:
233
- copy_module_layout(repo_dir, name_parts[:-1])
234
- else:
235
- repo_dir.mkdir(parents=True, exist_ok=True)
236
- # use the file name to name the file
237
- dest_path = repo_dir.joinpath(*name_parts[:-1]) / src_path.name
235
+ rel_path = module_file.relative_to(project_root)
236
+ dest_path = repo_dir / rel_path
238
237
  dest_path.parent.mkdir(parents=True, exist_ok=True)
239
238
  shutil.copy2(src_path, dest_path)
239
+
240
+ # Ensure __init__.py is copied over at every directory level
241
+ src_init = project_root / rel_path.parent / "__init__.py"
242
+ dest_init = dest_path.parent / "__init__.py"
243
+ if src_init.exists() and not dest_init.exists():
244
+ shutil.copy2(src_init, dest_init)
245
+
240
246
  return repo_dir
241
247
 
242
248
 
243
249
  def create_agent_repo(repo_path: str, with_code: bool = True) -> Path:
244
250
  """
251
+ Args:
252
+ repo_path: The path to the repository.
253
+ with_code: Whether to include the code in the repository.
254
+ branch: The branch to post it to.
255
+ tag: The tag to give it.
245
256
  Create a temporary directory inside the Modaic cache. Containing everything that will be pushed to the hub. This function adds the following files:
246
257
  - All internal modules used to run the agent
247
258
  - The pyproject.toml
@@ -339,3 +350,27 @@ def warn_if_local(sources: dict[str, dict]):
339
350
  f"Bundling agent with local package {source} installed from {config['path']}. This is not recommended.",
340
351
  stacklevel=5,
341
352
  )
353
+
354
+
355
+ def _module_path(instance: object) -> str:
356
+ """
357
+ Return a deterministic module path for the given instance.
358
+
359
+ Args:
360
+ instance: The object instance whose class path should be resolved.
361
+
362
+ Returns:
363
+ str: A fully qualified path in the form "<module>.<ClassName>". If the
364
+ class' module is "__main__", use the file system to derive a stable
365
+ module name: the parent directory name when the file is "__main__.py",
366
+ otherwise the file stem.
367
+ """
368
+
369
+ cls = type(instance)
370
+ module_name = cls.__module__
371
+ module = sys.modules[module_name]
372
+ file = Path(module.__file__)
373
+ module_path = str(file.relative_to(resolve_project_root()).with_suffix(""))
374
+ module_path = module_path.replace("/", ".")
375
+
376
+ return f"{module_path}.{cls.__name__}"
modaic/observability.py CHANGED
@@ -38,7 +38,7 @@ _configured = False
38
38
 
39
39
 
40
40
  def configure(
41
- tracing: bool = True,
41
+ tracing: bool = False,
42
42
  repo: Optional[str] = None,
43
43
  project: Optional[str] = None,
44
44
  base_url: str = "https://api.modaic.dev",
@@ -92,7 +92,7 @@ def configure(
92
92
  if project_name:
93
93
  _opik_client = Opik(host=base_url, project_name=project_name)
94
94
 
95
- config.update_session_config("track_disable", not tracing)
95
+ config.update_session_config("track_disable", not tracing)
96
96
 
97
97
  _configured = True
98
98
 
@@ -207,10 +207,17 @@ class Trackable:
207
207
  All Modaic classes except PrecompiledAgent should inherit from this class.
208
208
  """
209
209
 
210
- def __init__(self, repo: Optional[str] = None, project: Optional[str] = None, commit: Optional[str] = None):
210
+ def __init__(
211
+ self,
212
+ repo: Optional[str] = None,
213
+ project: Optional[str] = None,
214
+ commit: Optional[str] = None,
215
+ trace: bool = False,
216
+ ):
211
217
  self.repo = repo
212
218
  self.project = project
213
219
  self.commit = commit
220
+ self.trace = trace
214
221
 
215
222
  def set_repo_project(self, repo: Optional[str] = None, project: Optional[str] = None, trace: bool = True):
216
223
  """Update the repo and project for this trackable object."""
@@ -218,10 +225,7 @@ class Trackable:
218
225
  self.repo = repo
219
226
 
220
227
  self.project = f"{self.repo}-{project}" if project else self.repo
221
-
222
- # configure global tracing
223
- if trace and (repo or project):
224
- configure(tracing=trace, repo=repo or self.repo, project=project or self.project)
228
+ self.trace = trace
225
229
 
226
230
 
227
231
  MethodDecorator = Callable[
@@ -254,8 +258,8 @@ def track_modaic_obj(func: Callable[Concatenate[T, P], R]) -> Callable[Concatena
254
258
  repo = getattr(self, "repo", None)
255
259
  project = getattr(self, "project", None)
256
260
 
257
- # check if tracking is enabled globally
258
- if not _settings.tracing:
261
+ # check if tracking is enabled both globally and for this object
262
+ if not _settings.tracing or not self.trace:
259
263
  # binds the method to self so it can be called with args and kwars, also type cast's it to callable with type vars for static type checking
260
264
  bound = cast(Callable[P, R], func.__get__(self, type(self)))
261
265
  return bound(*args, **kwargs)
modaic/precompiled.py CHANGED
@@ -4,7 +4,15 @@ import os
4
4
  import pathlib
5
5
  from abc import ABC, abstractmethod
6
6
  from pathlib import Path
7
- from typing import TYPE_CHECKING, ClassVar, Dict, Generic, List, Optional, Type, TypeVar, Union
7
+ from typing import (
8
+ TYPE_CHECKING,
9
+ Dict,
10
+ List,
11
+ Optional,
12
+ Type,
13
+ TypeVar,
14
+ Union,
15
+ )
8
16
 
9
17
  import dspy
10
18
  from pydantic import BaseModel
@@ -13,6 +21,7 @@ from modaic.module_utils import create_agent_repo
13
21
  from modaic.observability import Trackable, track_modaic_obj
14
22
 
15
23
  from .hub import load_repo, push_folder_to_hub
24
+ from .module_utils import _module_path
16
25
 
17
26
  if TYPE_CHECKING:
18
27
  from modaic.context.base import Context
@@ -140,6 +149,7 @@ class PrecompiledAgent(dspy.Module):
140
149
  ):
141
150
  # create DSPy callback for observability if tracing is enabled
142
151
  callbacks = []
152
+ # FIXME This logic is not correct.
143
153
  if trace and (repo or project):
144
154
  try:
145
155
  from opik.integrations.dspy.callback import OpikCallback
@@ -150,7 +160,7 @@ class PrecompiledAgent(dspy.Module):
150
160
  elif repo and not project:
151
161
  project_name = repo
152
162
  else:
153
- raise ValueError("Must provide either repo to enable tracing")
163
+ raise ValueError("Must provide either repo to enable observability tracking")
154
164
 
155
165
  opik_callback = OpikCallback(project_name=project_name, log_graph=True)
156
166
  callbacks.append(opik_callback)
@@ -160,6 +170,7 @@ class PrecompiledAgent(dspy.Module):
160
170
 
161
171
  # initialize DSPy Module with callbacks
162
172
  super().__init__()
173
+ # FIXME this adds the same callback for every agent. Should only be the current agent.
163
174
  if callbacks:
164
175
  # set callbacks using DSPy's configuration
165
176
  import dspy
@@ -269,7 +280,11 @@ class PrecompiledAgent(dspy.Module):
269
280
  with_code: Whether to save the code along with the agent.json and config.json.
270
281
  """
271
282
  _push_to_hub(
272
- self, repo_path=repo_path, access_token=access_token, commit_message=commit_message, with_code=with_code
283
+ self,
284
+ repo_path=repo_path,
285
+ access_token=access_token,
286
+ commit_message=commit_message,
287
+ with_code=with_code,
273
288
  )
274
289
 
275
290
 
@@ -363,41 +378,6 @@ class Indexer(Retriever):
363
378
  pass
364
379
 
365
380
 
366
- def _module_path(instance: object) -> str:
367
- """
368
- Return a deterministic module path for the given instance.
369
-
370
- Args:
371
- instance: The object instance whose class path should be resolved.
372
-
373
- Returns:
374
- str: A fully qualified path in the form "<module>.<ClassName>". If the
375
- class' module is "__main__", use the file system to derive a stable
376
- module name: the parent directory name when the file is "__main__.py",
377
- otherwise the file stem.
378
- """
379
-
380
- cls = type(instance)
381
- module_name = getattr(cls, "__module__", "__main__")
382
- class_name = getattr(cls, "__name__", "Object")
383
- if module_name != "__main__":
384
- return f"{module_name}.{class_name}"
385
-
386
- # When executed as a script, classes often report __module__ == "__main__".
387
- # Normalize to a deterministic name based on the defining file path.
388
- try:
389
- file_path = pathlib.Path(inspect.getfile(cls)).resolve()
390
- except Exception:
391
- # Fallback to a generic name if the file cannot be determined
392
- normalized_root = "main"
393
- else:
394
- if file_path.name == "__main__.py":
395
- normalized_root = file_path.parent.name or "main"
396
- else:
397
- normalized_root = file_path.stem or "main"
398
- return f"{normalized_root}.{class_name}"
399
-
400
-
401
381
  # CAVEAT: PrecompiledConfig does not support push_to_hub() intentionally,
402
382
  # this is to avoid confusion when pushing a config to the hub thinking it
403
383
  # will update the config.json when in reality it will overwrite the entire
@@ -414,7 +394,12 @@ def _push_to_hub(
414
394
  """
415
395
  repo_dir = create_agent_repo(repo_path, with_code=with_code)
416
396
  self.save_precompiled(repo_dir, _with_auto_classes=with_code)
417
- push_folder_to_hub(repo_dir, repo_path=repo_path, access_token=access_token, commit_message=commit_message)
397
+ push_folder_to_hub(
398
+ repo_dir,
399
+ repo_path=repo_path,
400
+ access_token=access_token,
401
+ commit_message=commit_message,
402
+ )
418
403
 
419
404
 
420
405
  def is_local_path(s: str | Path) -> bool:
modaic/query_language.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from types import NoneType
2
- from typing import Any, Literal, Optional, Type, TypeAlias, Union
2
+ from typing import Optional, Type, TypeAlias, Union
3
3
 
4
- from langchain_core.structured_query import Comparator, Comparison, Operation, Operator, StructuredQuery, Visitor
4
+ from langchain_core.structured_query import Comparator, Comparison, Operation, Operator, Visitor
5
5
 
6
6
  ValueType: TypeAlias = Union[int, str, float, bool, NoneType, list, "Value"]
7
7
 
@@ -32,30 +32,6 @@ allowed_types = {
32
32
  }
33
33
 
34
34
 
35
- # def _print_return(func): # noqa: ANN001
36
- # def wrapper(*args, **kwargs):
37
- # result = func(*args, **kwargs)
38
- # if isinstance(op := args[1], str) and op[0] == "$":
39
- # if kwargs.get("recursed", False):
40
- # print( # noqa: T201
41
- # f"{repr(args[0])} ({mql_operator_to_python[op]}) {repr(args[2])} ->:",
42
- # result,
43
- # )
44
- # else:
45
- # print( # noqa: T201
46
- # f"{repr(args[0])} {mql_operator_to_python[op]} {repr(args[2])} ->:",
47
- # result,
48
- # )
49
- # else:
50
- # if func.__name__ == "__and__":
51
- # print(f"{repr(args[0])} & {repr(args[1])} ->:", result) # noqa: T201
52
- # elif func.__name__ == "__rand__":
53
- # print(f"{repr(args[1])} & {repr(args[0])} ->:", result) # noqa: T201
54
- # return result
55
-
56
- # return wrapper
57
-
58
-
59
35
  class Condition:
60
36
  """
61
37
  Modaic Query Language Property class.
modaic/types.py CHANGED
@@ -207,6 +207,8 @@ class InnerField(BaseModel):
207
207
  # NOTE: handle case where the float type was used and therefore not annotated with a format
208
208
  if is_optional:
209
209
  raise SchemaError("Array/List elements cannot be None/null")
210
+ if inspected_type["type"] == "object" or inspected_type["type"] == "array":
211
+ raise SchemaError("Arrays and Dicts are not supported for Array/List elements")
210
212
  return InnerField(
211
213
  type=inspected_type["type"],
212
214
  format=inspected_type.get("format", None),
modaic/utils.py CHANGED
@@ -2,9 +2,10 @@ import os
2
2
  import re
3
3
  from pathlib import Path
4
4
 
5
- from dotenv import load_dotenv
5
+ from dotenv import find_dotenv, load_dotenv
6
6
 
7
- load_dotenv()
7
+ env_file = find_dotenv(usecwd=True)
8
+ load_dotenv(env_file)
8
9
 
9
10
 
10
11
  def compute_cache_dir() -> Path:
@@ -18,4 +19,6 @@ def compute_cache_dir() -> Path:
18
19
 
19
20
  def validate_project_name(text: str) -> bool:
20
21
  """Letters, numbers, underscore, hyphen"""
21
- assert bool(re.match(r'^[a-zA-Z0-9_]+$', text)), "Invalid project name. Must contain only letters, numbers, and underscore."
22
+ assert bool(re.match(r"^[a-zA-Z0-9_]+$", text)), (
23
+ "Invalid project name. Must contain only letters, numbers, and underscore."
24
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modaic
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: Modular Agent Infrastructure Collective, a python framework for managing and sharing DSPy agents
5
5
  Author-email: Tyrin <tytodd@mit.edu>, Farouk <farouk@modaic.dev>
6
6
  License: MIT License
@@ -48,6 +48,7 @@ Requires-Dist: langchain-community>=0.3.29
48
48
  Requires-Dist: langchain-core>=0.3.72
49
49
  Requires-Dist: langchain-text-splitters>=0.3.9
50
50
  Requires-Dist: more-itertools>=10.8.0
51
+ Requires-Dist: openpyxl>=3.1.5
51
52
  Requires-Dist: opik==1.8.42
52
53
  Requires-Dist: pillow>=11.3.0
53
54
  Requires-Dist: pymilvus>=2.5.14
@@ -58,8 +59,11 @@ Requires-Dist: pinecone>=7.3.0; extra == "pinecone"
58
59
  Dynamic: license-file
59
60
 
60
61
  [![Docs](https://img.shields.io/badge/docs-available-brightgreen.svg)](https://docs.modaic.dev)
62
+ [![PyPI](https://img.shields.io/pypi/v/modaic)](https://pypi.org/project/modaic/)
63
+
64
+
61
65
  # Modaic 🐙
62
- **Mod**ular **A**gent **I**nfrastructure **C**ollective, a Python framework for building AI agents with structured context management, database integration, and retrieval-augmented generation (RAG) capabilities.
66
+ **Mod**ular **A**gent **I**nfrastructure **C**ollection, a Python framework for building AI agents with structured context management, database integration, and retrieval-augmented generation (RAG) capabilities.
63
67
 
64
68
  ## Overview
65
69
 
@@ -255,7 +259,6 @@ from modaic.databases import VectorDatabase, SQLDatabase
255
259
  from modaic.types import Indexer
256
260
 
257
261
  class TableRAGConfig(PrecompiledConfig):
258
- agent_type = "TableRAGAgent"
259
262
  k_recall: int = 50
260
263
  k_rerank: int = 5
261
264
 
@@ -1,23 +1,24 @@
1
- modaic/__init__.py,sha256=rf2O0S7OAz5fBkmOcwygsM3u8Nmq_ios01ToNweNnSk,639
2
- modaic/auto_agent.py,sha256=jegs8HMbE5OICZIDtBrP4kIma_R7rzy-1Kgmb_-eCck,8695
1
+ modaic/__init__.py,sha256=xHu2SUk3OMvb8PIzrVCRS1pBk-Ho9BhwmzKOf_bOjGc,809
2
+ modaic/auto.py,sha256=rPOdQ7s-YGBQLa_v6lVONH8pbOrarPwp4VzRErp0y5c,9091
3
+ modaic/datasets.py,sha256=K-PpPSYIxJI0-yH-SBVpk_EfCM9i_uPz-brmlzP7hzI,513
3
4
  modaic/exceptions.py,sha256=XxzxOWjZTzT3l1BqTr7coJnVGxJq53uppRNrqP__YGo,651
4
- modaic/hub.py,sha256=iWvWjaZxurd2BRXTS2gjhBvqVE1TDtwF0N7Falwj04Q,10527
5
- modaic/indexing.py,sha256=L0O5yV7AhDUJ0gMyGE17BvHN2gwHxwkOMaxNTkyWQ8g,4185
6
- modaic/module_utils.py,sha256=DDXUmcGFdaah_EhTlfdHC26ohmMwLNlIYo6PzYzKzqc,10728
7
- modaic/observability.py,sha256=QEjLbmsVQzWZuxKQU8TBMSHsHVGvTzOeoNlEn_srmyg,9955
8
- modaic/precompiled.py,sha256=g0AsFrHxzTlDETDqxZFqjHaLqW6m5OpGGyI01xDBT5U,15788
9
- modaic/query_language.py,sha256=c-La7jYhHgNyjlQaxz0ALvUoiCDGzH9DpSD_9cozxNQ,10463
10
- modaic/types.py,sha256=sHJ7J9YGfWIkDPfCuRc-n4O9n7g7LNnKSZperAviRFc,9905
11
- modaic/utils.py,sha256=zbeMlrP_hoo8JUKR_bcuYPxrlYcckY3p0KJ_h4CN5VQ,721
5
+ modaic/hub.py,sha256=d5HQjaE26K1qNCBc32qJtrpESyRv6OiniAteasiN_rk,11290
6
+ modaic/indexing.py,sha256=VdILiXiLVzgV1pSTV8Ho7x1dZtd31Y9z60d_Qtqr2NU,4195
7
+ modaic/module_utils.py,sha256=oJUOddvNGEIvEABOf7rzMB9erOkjDPm7FWLaBJ0XTSA,11797
8
+ modaic/observability.py,sha256=LgR4gJM4DhD-xlVX52mzRQSPgLQzbeh2LYPmQVqSh-A,9947
9
+ modaic/precompiled.py,sha256=cLRNoDB_ivawGftDpWQbkjjThrtID7bG1E-XVxi5kEM,14804
10
+ modaic/query_language.py,sha256=BJIigR0HLapiIn9fF7jM7PkLM8OWUDjwYuxmzcCVvyo,9487
11
+ modaic/types.py,sha256=gcx8J4oxrHwxA7McyYV4OKHsuPhhmowJtJIgjJQbLto,10081
12
+ modaic/utils.py,sha256=doJs-XL4TswSQFBINZeKrik-cvjZk-tS9XmWH8fOYiw,794
12
13
  modaic/agents/rag_agent.py,sha256=f8s3EILOPUxMpOKDoAvk-cfLE8S9kFNvkEcAC5z2EmQ,798
13
14
  modaic/agents/registry.py,sha256=z6GuPxGrq2dinCamiMJ_HVPsD9Tp9XWDUSMZ-uhWPrU,2446
14
- modaic/context/__init__.py,sha256=WTBo-WqhgeR84P3MEq0snLZyIHnLyv9VYO5Wfp4vrZo,534
15
- modaic/context/base.py,sha256=QrRNBs05fb5EXN4intHT4D-XIG76RzvrO6j8RXJpzd4,40380
15
+ modaic/context/__init__.py,sha256=FK-bxSu36yGFF1rATy4Yzl4Fpv9kYOlRpBRfr_4moiM,560
16
+ modaic/context/base.py,sha256=x66_lcdQ063DiluC6UnFEH4etgJkbAyukrgrp2KLV5U,40368
16
17
  modaic/context/dtype_mapping.py,sha256=xRasW-H92YEuOfH8SbsVnodM9F-90pazott8qF2GWHw,519
17
- modaic/context/table.py,sha256=c0OUVwglaDRtJ2Uo9z7bAKhIJpJXqwWk1VkKwQNOvUY,17696
18
- modaic/context/text.py,sha256=S8E-dbTe-Wip8KCZ5vcIOZVyiVQReIL4bSo0anQw828,2581
18
+ modaic/context/table.py,sha256=9Lh2_UyK3OsWmgUfZQa8jDeVAPAk_iueT89_cruDeSo,17699
19
+ modaic/context/text.py,sha256=gCVQx15FrPcmpr2GYkJca6noh7fw_nTcCt-hISwcnvQ,2581
19
20
  modaic/databases/__init__.py,sha256=-w_yiY-Sqi1SgcPD5oAQL7MU4VXTihPa1GYGlrHfsFw,784
20
- modaic/databases/graph_database.py,sha256=vMCYQrnBu5AIIGF_akkU9lWKFdbIDF3tlcyxiNQ_vSQ,9933
21
+ modaic/databases/graph_database.py,sha256=j44PgWGMeD3dtPZe5sRpKnruTA3snm_TiXncusTzqIQ,9990
21
22
  modaic/databases/sql_database.py,sha256=wqy7AqsalhmYsbNPy0FCAg1FrUKN6Bd8ytwyJireC94,12057
22
23
  modaic/databases/vector_database/__init__.py,sha256=sN1SuSAMC9NHJDOa80BN_olccaHgmiW2Ek57hBvdZWo,306
23
24
  modaic/databases/vector_database/vector_database.py,sha256=RsuRemgFV06opY26CekqLLRoAEFYOGl_CMuFETrYS0c,25238
@@ -32,8 +33,8 @@ modaic/databases/vector_database/vendors/qdrant.py,sha256=AbpHGcgLb-kRsJGnwFEktk
32
33
  modaic/storage/__init__.py,sha256=Zs-Y_9jfYUE8XVp8z-El0ZXFM_ZVMqM9aQ6fgGPZsf8,131
33
34
  modaic/storage/file_store.py,sha256=kSS7gTP_-16wR3Xgq3frF1BZ8Dw8N--kG4V9rrCXPcc,7315
34
35
  modaic/storage/pickle_store.py,sha256=fu9jkmmKNE852Y4R1NhOFePLfd2gskhHSXxuq1G1S3I,778
35
- modaic-0.1.2.dist-info/licenses/LICENSE,sha256=7LMx9j453Vz1DoQbFot8Uhp9SExF5wlOx7c8vw2qhsE,1333
36
- modaic-0.1.2.dist-info/METADATA,sha256=uwXUFpL8YCXXPaeDBVb5zc-luhpDvysCoyHP4VPsC2s,8569
37
- modaic-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
- modaic-0.1.2.dist-info/top_level.txt,sha256=RXWGuF-TsW8-17DveTJMPRiAgg_Rf2mq5F3R7tNu6t8,7
39
- modaic-0.1.2.dist-info/RECORD,,
36
+ modaic-0.2.0.dist-info/licenses/LICENSE,sha256=7LMx9j453Vz1DoQbFot8Uhp9SExF5wlOx7c8vw2qhsE,1333
37
+ modaic-0.2.0.dist-info/METADATA,sha256=71CN4H1DdXoN4f-ahbju8JqUaTU4zagjm-E__nIwesY,8651
38
+ modaic-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
39
+ modaic-0.2.0.dist-info/top_level.txt,sha256=RXWGuF-TsW8-17DveTJMPRiAgg_Rf2mq5F3R7tNu6t8,7
40
+ modaic-0.2.0.dist-info/RECORD,,
File without changes