aixtools 0.0.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 aixtools might be problematic. Click here for more details.

Files changed (88) hide show
  1. aixtools/.chainlit/config.toml +113 -0
  2. aixtools/.chainlit/translations/bn.json +214 -0
  3. aixtools/.chainlit/translations/en-US.json +214 -0
  4. aixtools/.chainlit/translations/gu.json +214 -0
  5. aixtools/.chainlit/translations/he-IL.json +214 -0
  6. aixtools/.chainlit/translations/hi.json +214 -0
  7. aixtools/.chainlit/translations/ja.json +214 -0
  8. aixtools/.chainlit/translations/kn.json +214 -0
  9. aixtools/.chainlit/translations/ml.json +214 -0
  10. aixtools/.chainlit/translations/mr.json +214 -0
  11. aixtools/.chainlit/translations/nl.json +214 -0
  12. aixtools/.chainlit/translations/ta.json +214 -0
  13. aixtools/.chainlit/translations/te.json +214 -0
  14. aixtools/.chainlit/translations/zh-CN.json +214 -0
  15. aixtools/__init__.py +11 -0
  16. aixtools/_version.py +34 -0
  17. aixtools/a2a/app.py +126 -0
  18. aixtools/a2a/google_sdk/__init__.py +0 -0
  19. aixtools/a2a/google_sdk/card.py +27 -0
  20. aixtools/a2a/google_sdk/pydantic_ai_adapter/agent_executor.py +199 -0
  21. aixtools/a2a/google_sdk/pydantic_ai_adapter/storage.py +26 -0
  22. aixtools/a2a/google_sdk/remote_agent_connection.py +88 -0
  23. aixtools/a2a/google_sdk/utils.py +59 -0
  24. aixtools/a2a/utils.py +115 -0
  25. aixtools/agents/__init__.py +12 -0
  26. aixtools/agents/agent.py +164 -0
  27. aixtools/agents/agent_batch.py +71 -0
  28. aixtools/agents/prompt.py +97 -0
  29. aixtools/app.py +143 -0
  30. aixtools/chainlit.md +14 -0
  31. aixtools/compliance/__init__.py +9 -0
  32. aixtools/compliance/private_data.py +138 -0
  33. aixtools/context.py +17 -0
  34. aixtools/db/__init__.py +17 -0
  35. aixtools/db/database.py +110 -0
  36. aixtools/db/vector_db.py +115 -0
  37. aixtools/google/client.py +25 -0
  38. aixtools/log_view/__init__.py +17 -0
  39. aixtools/log_view/app.py +195 -0
  40. aixtools/log_view/display.py +285 -0
  41. aixtools/log_view/export.py +51 -0
  42. aixtools/log_view/filters.py +41 -0
  43. aixtools/log_view/log_utils.py +26 -0
  44. aixtools/log_view/node_summary.py +229 -0
  45. aixtools/logfilters/__init__.py +7 -0
  46. aixtools/logfilters/context_filter.py +67 -0
  47. aixtools/logging/__init__.py +30 -0
  48. aixtools/logging/log_objects.py +227 -0
  49. aixtools/logging/logging_config.py +161 -0
  50. aixtools/logging/mcp_log_models.py +102 -0
  51. aixtools/logging/mcp_logger.py +172 -0
  52. aixtools/logging/model_patch_logging.py +87 -0
  53. aixtools/logging/open_telemetry.py +36 -0
  54. aixtools/mcp/__init__.py +9 -0
  55. aixtools/mcp/client.py +375 -0
  56. aixtools/mcp/example_client.py +30 -0
  57. aixtools/mcp/example_server.py +22 -0
  58. aixtools/mcp/fast_mcp_log.py +31 -0
  59. aixtools/mcp/faulty_mcp.py +319 -0
  60. aixtools/model_patch/model_patch.py +63 -0
  61. aixtools/server/__init__.py +29 -0
  62. aixtools/server/app_mounter.py +90 -0
  63. aixtools/server/path.py +72 -0
  64. aixtools/server/utils.py +70 -0
  65. aixtools/server/workspace_privacy.py +65 -0
  66. aixtools/testing/__init__.py +9 -0
  67. aixtools/testing/aix_test_model.py +149 -0
  68. aixtools/testing/mock_tool.py +66 -0
  69. aixtools/testing/model_patch_cache.py +279 -0
  70. aixtools/tools/doctor/__init__.py +3 -0
  71. aixtools/tools/doctor/tool_doctor.py +61 -0
  72. aixtools/tools/doctor/tool_recommendation.py +44 -0
  73. aixtools/utils/__init__.py +35 -0
  74. aixtools/utils/chainlit/cl_agent_show.py +82 -0
  75. aixtools/utils/chainlit/cl_utils.py +168 -0
  76. aixtools/utils/config.py +131 -0
  77. aixtools/utils/config_util.py +69 -0
  78. aixtools/utils/enum_with_description.py +37 -0
  79. aixtools/utils/files.py +17 -0
  80. aixtools/utils/persisted_dict.py +99 -0
  81. aixtools/utils/utils.py +167 -0
  82. aixtools/vault/__init__.py +7 -0
  83. aixtools/vault/vault.py +137 -0
  84. aixtools-0.0.0.dist-info/METADATA +669 -0
  85. aixtools-0.0.0.dist-info/RECORD +88 -0
  86. aixtools-0.0.0.dist-info/WHEEL +5 -0
  87. aixtools-0.0.0.dist-info/entry_points.txt +2 -0
  88. aixtools-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,35 @@
1
+ """
2
+ Utils package initialization.
3
+ """
4
+
5
+ from aixtools.logging.logging_config import get_logger # pylint: disable=import-error
6
+ from aixtools.utils import config
7
+ from aixtools.utils.enum_with_description import EnumWithDescription
8
+ from aixtools.utils.persisted_dict import PersistedDict
9
+ from aixtools.utils.utils import (
10
+ escape_backticks,
11
+ escape_newline,
12
+ find_file,
13
+ prepend_all_lines,
14
+ remove_quotes,
15
+ tabit,
16
+ to_str,
17
+ tripple_quote_strip,
18
+ truncate,
19
+ )
20
+
21
+ __all__ = [
22
+ "config",
23
+ "PersistedDict",
24
+ "EnumWithDescription",
25
+ "escape_newline",
26
+ "escape_backticks",
27
+ "find_file",
28
+ "get_logger",
29
+ "prepend_all_lines",
30
+ "remove_quotes",
31
+ "tabit",
32
+ "to_str",
33
+ "truncate",
34
+ "tripple_quote_strip",
35
+ ]
@@ -0,0 +1,82 @@
1
+ import chainlit as cl
2
+ import rich
3
+ from pydantic_ai import Agent
4
+ from pydantic_ai.messages import (
5
+ FinalResultEvent,
6
+ FunctionToolCallEvent,
7
+ FunctionToolResultEvent,
8
+ PartDeltaEvent,
9
+ PartStartEvent,
10
+ TextPartDelta,
11
+ ToolCallPartDelta,
12
+ )
13
+
14
+ from aixtools.logging.log_objects import ObjectLogger
15
+
16
+
17
+ def _show_debug_info(debug, *args):
18
+ if debug:
19
+ rich.print(*args)
20
+
21
+
22
+ async def show_run(agent: Agent, prompt, msg: cl.Message, debug=False, verbose=True): # noqa: PLR0912
23
+ """Run an agent with a prompt and send the results to a message."""
24
+ nodes = []
25
+ async with agent.iter(prompt) as run:
26
+ with ObjectLogger(debug=debug, verbose=verbose) as agent_logger:
27
+ async for node in run:
28
+ nodes.append(node)
29
+ agent_logger.log(node)
30
+ if Agent.is_user_prompt_node(node):
31
+ # A user prompt node => The user has provided input
32
+ _show_debug_info(debug, "=== UserPromptNode: ", node)
33
+ elif Agent.is_model_request_node(node):
34
+ # A model request node => We can stream tokens from the model's request
35
+ _show_debug_info(debug, "=== ModelRequestNode: streaming partial request tokens ===")
36
+ async with node.stream(run.ctx) as request_stream:
37
+ async for event in request_stream:
38
+ if isinstance(event, PartStartEvent):
39
+ _show_debug_info(debug, f"[Request] Starting part {event.index}: ", event.part)
40
+ elif isinstance(event, PartDeltaEvent):
41
+ if isinstance(event.delta, TextPartDelta):
42
+ _show_debug_info(
43
+ debug,
44
+ (
45
+ "[ModelRequestNone / PartDeltaEvent / TextPartDelta] "
46
+ f"Part {event.index}: {event.delta.content_delta}"
47
+ ),
48
+ )
49
+ await msg.stream_token(event.delta.content_delta)
50
+ elif isinstance(event.delta, ToolCallPartDelta):
51
+ _show_debug_info(
52
+ debug,
53
+ f"[ModelRequestNone / PartDeltaEvent / ToolCallPartDelta] Part {event.index}, ",
54
+ event.delta,
55
+ )
56
+ elif isinstance(event, FinalResultEvent):
57
+ _show_debug_info(
58
+ debug, f"[Result] The model produced a final result (tool_name={event.tool_name})"
59
+ )
60
+ elif Agent.is_call_tools_node(node):
61
+ # A handle-response node => The model returned some data, potentially calls a tool
62
+ _show_debug_info(debug, "=== CallToolsNode: streaming partial response & tool usage ===")
63
+ async with node.stream(run.ctx) as handle_stream:
64
+ async for event in handle_stream:
65
+ if isinstance(event, FunctionToolCallEvent):
66
+ _show_debug_info(
67
+ debug,
68
+ (
69
+ f"[Tools] The LLM calls tool={event.part.tool_name!r} "
70
+ f"with args={event.part.args} (tool_call_id={event.part.tool_call_id!r})"
71
+ ),
72
+ )
73
+ elif isinstance(event, FunctionToolResultEvent):
74
+ _show_debug_info(
75
+ debug,
76
+ f"[Tools] Tool call {event.tool_call_id!r} returned => {event.result.content}",
77
+ )
78
+ elif Agent.is_end_node(node):
79
+ assert run.result.output == node.data.output
80
+ # Once an End node is reached, the agent run is complete
81
+ _show_debug_info(debug, f"=== Final Agent Output: {run.result.output} ===")
82
+ return run.result.output
@@ -0,0 +1,168 @@
1
+ """
2
+ Utilities for Chainlit
3
+ """
4
+
5
+ import inspect
6
+ from copy import deepcopy
7
+ from functools import wraps
8
+ from typing import Callable, List, Optional, Union
9
+
10
+ import pandas as pd
11
+ from chainlit import Step
12
+ from chainlit.context import get_context
13
+ from literalai.observability.step import TrueStepType
14
+
15
+ from aixtools.logging.logging_config import get_logger
16
+ from aixtools.utils.utils import truncate
17
+
18
+ logger = get_logger(__name__)
19
+
20
+ DEFAULT_SKIP_ARGS = ("self", "cls")
21
+
22
+ MAX_SIZE_STR = 10 * 1024
23
+ MAX_SIZE_DF_ROWS = 100
24
+
25
+
26
+ def is_chainlit() -> bool:
27
+ """Are we running in chainlit?"""
28
+ try:
29
+ get_context()
30
+ return True
31
+ except Exception:
32
+ return False
33
+
34
+
35
+ def flatten_args_kwargs(func, args, kwargs, skip_args=DEFAULT_SKIP_ARGS):
36
+ signature = inspect.signature(func)
37
+ bound_arguments = signature.bind(*args, **kwargs)
38
+ bound_arguments.apply_defaults()
39
+ return {k: deepcopy(v) for k, v in bound_arguments.arguments.items() if k not in skip_args}
40
+
41
+
42
+ def _step_name(func, args, kwargs):
43
+ """
44
+ Create a step name: class.method
45
+ It detects the class name from the first method's argument.
46
+ """
47
+ if len(args) == 0:
48
+ return func.__name__
49
+ signature = inspect.signature(func)
50
+ bound_arguments = signature.bind(*args, **kwargs)
51
+ arguments = [(k, v) for k, v in bound_arguments.arguments.items()]
52
+ arg0_name, arg0_value = arguments[0]
53
+ if arg0_name == "self":
54
+ return f"{arg0_value.__class__.__name__}.{func.__name__}"
55
+ if arg0_name == "cls":
56
+ return f"{arg0_value.__name__}.{func.__name__}"
57
+ return func.__name__
58
+
59
+
60
+ def limit_size(data):
61
+ """ """
62
+ if isinstance(data, str):
63
+ return truncate(data, max_len=MAX_SIZE_STR)
64
+ if isinstance(data, pd.DataFrame):
65
+ if len(data) > MAX_SIZE_DF_ROWS:
66
+ return data.head(MAX_SIZE_DF_ROWS)
67
+ return data
68
+
69
+
70
+ def cl_step( # noqa: PLR0913
71
+ original_function: Optional[Callable] = None,
72
+ *,
73
+ name: Optional[str] = "",
74
+ type: TrueStepType = "undefined",
75
+ id: Optional[str] = None,
76
+ parent_id: Optional[str] = None,
77
+ tags: Optional[List[str]] = None,
78
+ language: Optional[str] = None,
79
+ show_input: Union[bool, str] = "json",
80
+ default_open: bool = False,
81
+ ):
82
+ """
83
+ Step decorator for async and sync functions and methods (they ignore the self argument).
84
+ It deactivates if not within a Chainlit context.
85
+ """
86
+
87
+ def wrapper(func: Callable):
88
+ # Handle async decorator
89
+ if inspect.iscoroutinefunction(func):
90
+
91
+ @wraps(func)
92
+ async def async_wrapper(*args, **kwargs):
93
+ nonlocal name
94
+ if not name:
95
+ name = _step_name(func, args, kwargs)
96
+ if is_chainlit():
97
+ async with Step(
98
+ type=type,
99
+ name=name,
100
+ id=id,
101
+ parent_id=parent_id,
102
+ tags=tags,
103
+ language=language,
104
+ show_input=show_input,
105
+ default_open=default_open,
106
+ ) as step:
107
+ try:
108
+ step.input = flatten_args_kwargs(func, args, kwargs)
109
+ except Exception as e:
110
+ logger.exception(e)
111
+ result = await func(*args, **kwargs)
112
+ try:
113
+ if result and not step.output:
114
+ step.output = limit_size(result)
115
+ except Exception as e:
116
+ step.is_error = True
117
+ step.output = str(e)
118
+ return result
119
+ else:
120
+ # If not in Chainlit, just call the function
121
+ result = await func(*args, **kwargs)
122
+ print(f"Function '{func.__name__}' called with args: {args}, kwargs: {kwargs}, result: {result}")
123
+ return result
124
+
125
+ return async_wrapper
126
+ else:
127
+ # Handle sync decorator
128
+ @wraps(func)
129
+ def sync_wrapper(*args, **kwargs):
130
+ nonlocal name
131
+ if not name:
132
+ name = _step_name(func, args, kwargs)
133
+ if is_chainlit():
134
+ with Step(
135
+ type=type,
136
+ name=name,
137
+ id=id,
138
+ parent_id=parent_id,
139
+ tags=tags,
140
+ language=language,
141
+ show_input=show_input,
142
+ default_open=default_open,
143
+ ) as step:
144
+ try:
145
+ step.input = flatten_args_kwargs(func, args, kwargs)
146
+ except Exception as e:
147
+ logger.exception(e)
148
+ result = func(*args, **kwargs)
149
+ try:
150
+ if result and not step.output:
151
+ step.output = limit_size(result)
152
+ except Exception as e:
153
+ step.is_error = True
154
+ step.output = str(e)
155
+ return result
156
+ else:
157
+ # If not in Chainlit, just call the function
158
+ result = func(*args, **kwargs)
159
+ print(f"Function '{func.__name__}' called with args: {args}, kwargs: {kwargs}, result: {result}")
160
+ return result
161
+
162
+ return sync_wrapper
163
+
164
+ func = original_function
165
+ if not func:
166
+ return wrapper
167
+ else:
168
+ return wrapper(func)
@@ -0,0 +1,131 @@
1
+ """
2
+ Configuration settings and environment variables for the application.
3
+ """
4
+
5
+ import logging
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from dotenv import dotenv_values, load_dotenv
10
+
11
+ from aixtools.utils.config_util import find_env_file, get_project_root, get_variable_env
12
+ from aixtools.utils.utils import str2bool
13
+
14
+ # Debug mode
15
+ LOG_LEVEL = logging.DEBUG
16
+
17
+ # Set up some environment variables (there are usually set up by 'config.sh')
18
+
19
+ # This file's path
20
+ FILE_PATH = Path(__file__).resolve()
21
+
22
+ # This project's root directory (AixTools)
23
+ # if installed as a package, it will be `.venv/lib/python3.x/site-packages/aixtools`
24
+ PROJECT_DIR = FILE_PATH.parent.parent.parent.resolve()
25
+
26
+ # Get the main project directory (the one project that is using this package)
27
+ PROJECT_ROOT = get_project_root()
28
+
29
+ # From the environment variables
30
+
31
+
32
+ # Iterate over all parents of FILE_PATH to find .env files
33
+ def all_parents(path: Path):
34
+ """Yield all parent directories of a given path."""
35
+ while path.parent != path:
36
+ yield path
37
+ path = path.parent
38
+
39
+
40
+ # Set up environment search path
41
+ # Start with the most specific (current directory) and expand outward
42
+ env_dirs = [Path.cwd(), PROJECT_ROOT, FILE_PATH.parent]
43
+ env_file = find_env_file(env_dirs)
44
+
45
+ if env_file:
46
+ logging.info("Using .env file at '%s'", env_file)
47
+ # Load the environment variables from the found .env file
48
+ load_dotenv(env_file)
49
+ # Assign project dir based on the .env file
50
+ MAIN_PROJECT_DIR = Path(env_file).parent
51
+ logging.info("Using MAIN_PROJECT_DIR='%s'", MAIN_PROJECT_DIR)
52
+ # Assign variables in '.env' global python environment
53
+ env_vars = dotenv_values(env_file)
54
+ globals().update(env_vars)
55
+ else:
56
+ logging.error("No '.env' file found in any of the search paths, or their parents: %s", env_dirs)
57
+ sys.exit(1)
58
+
59
+
60
+ # ---
61
+ # Directories
62
+ # ---
63
+ SCRIPTS_DIR = MAIN_PROJECT_DIR / "scripts"
64
+ DATA_DIR = Path(get_variable_env("DATA_DIR") or MAIN_PROJECT_DIR / "data")
65
+ DATA_DB_DIR = Path(get_variable_env("DATA_DB_DIR", default=DATA_DIR / "db"))
66
+ LOGS_DIR = MAIN_PROJECT_DIR / "logs"
67
+
68
+ logging.warning("Using DATA_DIR='%s'", DATA_DIR)
69
+
70
+ # Vector database
71
+ VDB_CHROMA_PATH = DATA_DB_DIR / "chroma.db"
72
+ VDB_DEFAULT_SIMILARITY_THRESHOLD = 0.85
73
+
74
+
75
+ # ---
76
+ # Variables in '.env' file
77
+ # Explicitly load specific variables
78
+ # ---
79
+
80
+ MODEL_TIMEOUT = int(get_variable_env("MODEL_TIMEOUT", default="120")) # type: ignore
81
+
82
+ MODEL_FAMILY = get_variable_env("MODEL_FAMILY")
83
+
84
+ # Azure models
85
+ AZURE_MODEL_NAME = get_variable_env("AZURE_MODEL_NAME")
86
+ AZURE_OPENAI_ENDPOINT = get_variable_env("AZURE_OPENAI_ENDPOINT")
87
+ AZURE_OPENAI_API_KEY = get_variable_env("AZURE_OPENAI_API_KEY")
88
+ AZURE_OPENAI_API_VERSION = get_variable_env("AZURE_OPENAI_API_VERSION")
89
+
90
+ # OpenAI models
91
+ OPENAI_API_KEY = get_variable_env("OPENAI_API_KEY")
92
+ OPENAI_MODEL_NAME = get_variable_env("OPENAI_MODEL_NAME")
93
+
94
+ # Ollama models
95
+ OLLAMA_URL = get_variable_env("OLLAMA_URL")
96
+ OLLAMA_MODEL_NAME = get_variable_env("OLLAMA_MODEL_NAME")
97
+
98
+ # OpenRouter models
99
+ OPENROUTER_API_KEY = get_variable_env("OPENROUTER_API_KEY")
100
+ OPENROUTER_API_URL = get_variable_env("OPENROUTER_API_URL", default="https://openrouter.ai/api/v1")
101
+ OPENROUTER_MODEL_NAME = get_variable_env("OPENROUTER_MODEL_NAME")
102
+
103
+ # Embeddings
104
+ VDB_EMBEDDINGS_MODEL_FAMILY = get_variable_env("VDB_EMBEDDINGS_MODEL_FAMILY")
105
+ OPENAI_VDB_EMBEDDINGS_MODEL_NAME = get_variable_env("OPENAI_VDB_EMBEDDINGS_MODEL_NAME")
106
+ AZURE_VDB_EMBEDDINGS_MODEL_NAME = get_variable_env("AZURE_VDB_EMBEDDINGS_MODEL_NAME")
107
+ OLLAMA_VDB_EMBEDDINGS_MODEL_NAME = get_variable_env("OLLAMA_VDB_EMBEDDINGS_MODEL_NAME")
108
+
109
+ # Bedrock models
110
+ AWS_ACCESS_KEY_ID = get_variable_env("AWS_ACCESS_KEY_ID", allow_empty=True)
111
+ AWS_SECRET_ACCESS_KEY = get_variable_env("AWS_SECRET_ACCESS_KEY", allow_empty=True)
112
+ AWS_SESSION_TOKEN = get_variable_env("AWS_SESSION_TOKEN", allow_empty=True)
113
+ AWS_REGION = get_variable_env("AWS_REGION", allow_empty=True, default="us-east-1")
114
+ AWS_PROFILE = get_variable_env("AWS_PROFILE", allow_empty=True)
115
+ BEDROCK_MODEL_NAME = get_variable_env("BEDROCK_MODEL_NAME", allow_empty=True)
116
+
117
+ # LogFire
118
+ LOGFIRE_TOKEN = get_variable_env("LOGFIRE_TOKEN", True, "")
119
+ LOGFIRE_TRACES_ENDPOINT = get_variable_env("LOGFIRE_TRACES_ENDPOINT", True, "")
120
+
121
+ # Google Vertex AI
122
+ GOOGLE_GENAI_USE_VERTEXAI = str2bool(get_variable_env("GOOGLE_GENAI_USE_VERTEXAI", True, True))
123
+ GOOGLE_CLOUD_PROJECT = get_variable_env("GOOGLE_CLOUD_PROJECT", True)
124
+ GOOGLE_CLOUD_LOCATION = get_variable_env("GOOGLE_CLOUD_LOCATION", True)
125
+
126
+ # vault parameters.
127
+ VAULT_ADDRESS = get_variable_env("VAULT_ADDRESS", default="http://localhost:8200")
128
+ VAULT_TOKEN = get_variable_env("VAULT_TOKEN", default="vault-token")
129
+ VAULT_ENV = get_variable_env("ENV", default="dev")
130
+ VAULT_MOUNT_POINT = get_variable_env("VAULT_MOUNT_POINT", default="secret")
131
+ VAULT_PATH_PREFIX = get_variable_env("VAULT_PATH_PREFIX", default="path")
@@ -0,0 +1,69 @@
1
+ """
2
+ Utility functions for configuration management and environment variables.
3
+ """
4
+
5
+ import logging
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from dotenv import find_dotenv
11
+
12
+
13
+ def get_project_root() -> Path:
14
+ """
15
+ Return the directory where the main script lives.
16
+ Falls back to the current working directory if run interactively.
17
+ """
18
+ main_mod = sys.modules.get("__main__")
19
+ main_file = getattr(main_mod, "__file__", None)
20
+ if main_file:
21
+ return Path(main_file).resolve().parent
22
+
23
+ # no __file__ (e.g. interactive shell); assume cwd is the project root
24
+ return Path.cwd()
25
+
26
+
27
+ def all_parents(path: Path):
28
+ """Yield all parent directories of a given path."""
29
+ while path.parent != path:
30
+ yield path
31
+ path = path.parent
32
+
33
+
34
+ def find_env_file(env_search_dirs: list[Path]):
35
+ """Find the first .env file in the given list of paths and their parents."""
36
+ env_file = find_dotenv()
37
+ logging.warning("Looking for '.env' file in default directory")
38
+ if env_file:
39
+ return env_file
40
+ # Find all parents of the paths
41
+ for search_dir in env_search_dirs:
42
+ # '.env' file in this directory?
43
+ logging.warning("Looking for '.env' file at '%s'", search_dir)
44
+ env_file = find_dotenv(str(search_dir / ".env"))
45
+ if env_file:
46
+ return env_file
47
+ # Try all parents of this dir
48
+ for parent_dir in all_parents(search_dir):
49
+ logging.warning("Looking for '.env' file at '%s'", parent_dir)
50
+ env_file = find_dotenv(str(parent_dir / ".env"))
51
+ if env_file:
52
+ return env_file
53
+ return None
54
+
55
+
56
+ def get_variable_env(name: str, allow_empty=True, default=None) -> str | None:
57
+ """Retrieve environment variable with optional validation and default value."""
58
+ val = os.environ.get(name, default)
59
+ if not allow_empty and ((val is None) or (val == "")):
60
+ raise ValueError(f"Environment variable {name} is not set")
61
+ return val
62
+
63
+
64
+ def set_variable_env(name: str, val: str) -> str:
65
+ """Set environment variable and validate it's not None."""
66
+ os.environ[name] = val
67
+ if val is None:
68
+ raise ValueError(f"Environment variable {name} is set to None")
69
+ return val
@@ -0,0 +1,37 @@
1
+ """
2
+ Enhanced Enum implementation that supports descriptions for enum values.
3
+ """
4
+
5
+ from enum import Enum
6
+
7
+
8
+ class EnumWithDescription(str, Enum):
9
+ """
10
+ An enum with string values and descriptions.
11
+ Each enum value has a string representation and a description.
12
+
13
+ Example:
14
+ class MyEnum(EnumWithDescription):
15
+ VALUE1 = "value1", "This is a description for VALUE1"
16
+ VALUE2 = "value2", "This is a description for VALUE2"
17
+ VALUE3 = "value3", "This is a description for VALUE3"
18
+
19
+ print(MyEnum.describe())
20
+ # Output:
21
+ # VALUE1: This is a description for VALUE1
22
+ # VALUE2: This is a description for VALUE2
23
+ # VALUE3: This is a description for VALUE3
24
+ """
25
+
26
+ @classmethod
27
+ def describe(cls) -> str:
28
+ """
29
+ Get the description of a decision's enum values
30
+ """
31
+ return "\n".join([f"{field.name}: {field.__doc__}" for field in cls])
32
+
33
+ def __new__(cls, value, doc):
34
+ obj = str.__new__(cls, value)
35
+ obj._value_ = value
36
+ obj.__doc__ = doc
37
+ return obj
@@ -0,0 +1,17 @@
1
+ """File utilities"""
2
+
3
+
4
+ def is_text_content(data: bytes, mime_type: str) -> bool:
5
+ """Check if content is text based on mime type and content analysis."""
6
+ # Check mime type first
7
+ if mime_type and (
8
+ mime_type.startswith("text/") or mime_type in ["application/json", "application/xml", "application/javascript"]
9
+ ):
10
+ return True
11
+
12
+ # Try to decode as UTF-8 to check if it's text
13
+ try:
14
+ data.decode("utf-8")
15
+ return True
16
+ except UnicodeDecodeError:
17
+ return False
@@ -0,0 +1,99 @@
1
+ """
2
+ Dictionary implementation that automatically persists its contents to disk.
3
+ """
4
+
5
+ import json
6
+ import pickle
7
+ from pathlib import Path
8
+
9
+ from aixtools.logging.logging_config import get_logger
10
+
11
+ logger = get_logger(__name__)
12
+
13
+ DATA_KEY = "__dictionary_data__"
14
+
15
+
16
+ class PersistedDict(dict):
17
+ """
18
+ A dictionary that persists to a file on disk as JSON.
19
+ Keys are always converted to strings.
20
+ """
21
+
22
+ def __init__(self, file_path: Path):
23
+ self.file_path = file_path if isinstance(file_path, Path) else Path(file_path)
24
+ self.use_pickle = None
25
+ if file_path.suffix == ".json":
26
+ self.use_pickle = False
27
+ elif file_path.suffix == ".pkl":
28
+ self.use_pickle = True
29
+ else:
30
+ raise ValueError(f"Unsupported file extension '{file_path.suffix}' for file '{file_path}'")
31
+ self.load()
32
+
33
+ def __contains__(self, key):
34
+ return super().__contains__(str(key))
35
+
36
+ def __delitem__(self, key):
37
+ super().__delitem__(str(key))
38
+ self.save()
39
+
40
+ def get(self, key, default=None):
41
+ return super().get(str(key), default)
42
+
43
+ def __getitem__(self, key):
44
+ return super().__getitem__(str(key))
45
+
46
+ def load(self):
47
+ """Load dictionary data from disk using either pickle or JSON format."""
48
+ if self.use_pickle:
49
+ self._load_pickle()
50
+ else:
51
+ self._load_json()
52
+
53
+ def _load_json(self):
54
+ try:
55
+ with open(self.file_path, "r", encoding="utf-8") as f:
56
+ self.update(json.load(f))
57
+ logger.debug("Persistent dictionary: Loaded %d items from JSON file '%s'", len(self), self.file_path)
58
+ except FileNotFoundError:
59
+ pass
60
+
61
+ def _load_pickle(self):
62
+ try:
63
+ with open(self.file_path, "rb") as f:
64
+ object_data = pickle.load(f)
65
+ for k, v in object_data[DATA_KEY].items():
66
+ super().__setitem__(str(k), v)
67
+ for k, v in object_data.items():
68
+ if k != DATA_KEY:
69
+ self.__dict__[k] = v
70
+ logger.debug("Persistent dictionary: Loaded %d items from pickle file '%s'", len(self), self.file_path)
71
+ except FileNotFoundError:
72
+ pass
73
+
74
+ def save(self):
75
+ """Save dictionary data to disk using either pickle or JSON format."""
76
+ if self.use_pickle:
77
+ self._save_pickle()
78
+ else:
79
+ self._save_json()
80
+
81
+ def _save_json(self):
82
+ self.file_path.parent.mkdir(parents=True, exist_ok=True)
83
+ with open(self.file_path, "w", encoding="utf-8") as f:
84
+ json.dump(self, f, indent=2)
85
+
86
+ def _save_pickle(self):
87
+ self.file_path.parent.mkdir(parents=True, exist_ok=True)
88
+ with open(self.file_path, "wb") as f:
89
+ object_data = dict(self.__dict__)
90
+ object_data[DATA_KEY] = dict(self)
91
+ pickle.dump(object_data, f)
92
+
93
+ def __setitem__(self, key, value):
94
+ super().__setitem__(str(key), value)
95
+ self.save()
96
+
97
+ def update(self, *args, **kwargs):
98
+ super().update(*args, **kwargs)
99
+ self.save()