agently 4.0.7__py3-none-any.whl → 4.0.7.2__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.
- agently/_default_init.py +4 -0
- agently/_default_settings.yaml +3 -1
- agently/base.py +19 -1
- agently/builtins/agent_extensions/ChatSessionExtension.py +2 -2
- agently/builtins/agent_extensions/SessionExtension.py +294 -0
- agently/builtins/agent_extensions/__init__.py +1 -0
- agently/builtins/plugins/PromptGenerator/AgentlyPromptGenerator.py +57 -17
- agently/builtins/plugins/Session/AgentlyMemoSession.py +652 -0
- agently/builtins/tools/Browse.py +11 -3
- agently/builtins/tools/Cmd.py +112 -0
- agently/builtins/tools/Search.py +28 -2
- agently/builtins/tools/__init__.py +1 -0
- agently/core/Agent.py +7 -7
- agently/core/ModelRequest.py +6 -5
- agently/core/Prompt.py +1 -1
- agently/core/Session.py +85 -0
- agently/core/TriggerFlow/TriggerFlow.py +1 -1
- agently/core/TriggerFlow/process/BaseProcess.py +8 -4
- agently/integrations/chromadb.py +4 -4
- agently/types/data/__init__.py +2 -0
- agently/types/data/prompt.py +6 -1
- agently/types/data/tool.py +9 -0
- agently/types/plugins/BuiltInTool.py +22 -0
- agently/types/plugins/Session.py +159 -0
- agently/types/plugins/__init__.py +21 -0
- agently/types/plugins/base.py +1 -1
- agently/utils/AGENT_UTILS_GUIDE.md +175 -0
- agently/utils/DataFormatter.py +14 -4
- agently/utils/DataLocator.py +108 -31
- agently/utils/FunctionShifter.py +3 -2
- agently/utils/TimeInfo.py +22 -0
- agently/utils/__init__.py +1 -0
- agently-4.0.7.2.dist-info/METADATA +433 -0
- {agently-4.0.7.dist-info → agently-4.0.7.2.dist-info}/RECORD +36 -28
- {agently-4.0.7.dist-info → agently-4.0.7.2.dist-info}/WHEEL +1 -1
- agently-4.0.7.dist-info/METADATA +0 -194
- {agently-4.0.7.dist-info → agently-4.0.7.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Agently Utils Guide (Agent-Readable)
|
|
2
|
+
|
|
3
|
+
Use this as a compact, agent-oriented guide to the utilities in `agently/utils`. It is intentionally brief and practical.
|
|
4
|
+
|
|
5
|
+
## Quick Map (TL;DR)
|
|
6
|
+
- data shaping: `DataFormatter`, `RuntimeData`, `SerializableRuntimeData`, `Settings`
|
|
7
|
+
- path and JSON helpers: `DataLocator`, `DataPathBuilder`, `StreamingJSONCompleter`, `StreamingJSONParser`
|
|
8
|
+
- async/sync bridging: `FunctionShifter`, `GeneratorConsumer`
|
|
9
|
+
- dynamic deps: `LazyImport`
|
|
10
|
+
- storage: `Storage`, `AsyncStorage`
|
|
11
|
+
- misc: `Logger`, `Messenger`, `PythonSandbox`
|
|
12
|
+
- legacy: `old_RuntimeData` (avoid unless you must keep backward behavior)
|
|
13
|
+
|
|
14
|
+
## Utilities
|
|
15
|
+
|
|
16
|
+
### DataFormatter
|
|
17
|
+
Purpose: normalize complex values into safe, serializable, or string forms, and do placeholder substitution.
|
|
18
|
+
|
|
19
|
+
Key methods:
|
|
20
|
+
- `sanitize(value, remain_type=False)`: convert complex objects into JSON-ish values. Handles `datetime`, `RuntimeData`, `pydantic.BaseModel`, and typing constructs like `list[T]`, `Union`, `Literal`.
|
|
21
|
+
- `to_str_key_dict(value, value_format=None, default_key=None, default_value=None)`: ensure dict keys are strings and values optionally sanitized or stringified. If input is not a dict, can wrap with `default_key`.
|
|
22
|
+
- `from_schema_to_kwargs_format(schema)`: convert JSON Schema object fields into Agently kwargs-style `(type, desc)` mapping.
|
|
23
|
+
- `substitute_placeholder(obj, variable_mappings, placeholder_pattern=None)`: recursive replace `${key}` placeholders in strings, dicts, lists, sets, tuples.
|
|
24
|
+
|
|
25
|
+
When to use:
|
|
26
|
+
- Before logging/serialization.
|
|
27
|
+
- When building structured prompts from schemas.
|
|
28
|
+
- When injecting env variables into settings or prompts.
|
|
29
|
+
|
|
30
|
+
### DataLocator
|
|
31
|
+
Purpose: locate values by path, and extract JSON blocks from mixed text.
|
|
32
|
+
|
|
33
|
+
Key methods:
|
|
34
|
+
- `locate_path_in_dict(dict, path, style="dot"|"slash", default=None)`: safe deep lookup with `a.b[0]` or `/a/b/0` styles.
|
|
35
|
+
- `locate_all_json(text)`: scan text and return all JSON-like blocks.
|
|
36
|
+
- `locate_output_json(text, output_prompt_dict)`: pick the most likely JSON block matching your output schema.
|
|
37
|
+
|
|
38
|
+
When to use:
|
|
39
|
+
- Parsing LLM responses that mix text + JSON.
|
|
40
|
+
- Robust extraction for streaming parsers.
|
|
41
|
+
|
|
42
|
+
### DataPathBuilder
|
|
43
|
+
Purpose: convert and reason about dot/slash paths, and extract expected parsing paths from a schema-like dict.
|
|
44
|
+
|
|
45
|
+
Key methods:
|
|
46
|
+
- `build_dot_path(keys)`, `build_slash_path(keys)`
|
|
47
|
+
- `convert_dot_to_slash(dot_path)`, `convert_slash_to_dot(slash_path)`
|
|
48
|
+
- `extract_possible_paths(schema, style="dot")`: find all possible paths.
|
|
49
|
+
- `extract_parsing_key_orders(schema, style="dot")`: paths in definition order (used by streaming parser).
|
|
50
|
+
- `get_value_by_path(data, path, style="dot")`: retrieve values, supports `[*]` wildcard expansion.
|
|
51
|
+
|
|
52
|
+
When to use:
|
|
53
|
+
- Streaming JSON parsing with ordered fields.
|
|
54
|
+
- Mapping UI updates to schema paths.
|
|
55
|
+
|
|
56
|
+
### FunctionShifter
|
|
57
|
+
Purpose: bridge sync/async code and run async work safely from sync contexts.
|
|
58
|
+
|
|
59
|
+
Key methods:
|
|
60
|
+
- `syncify(func)`: wrap an async function so it can be called in sync code. Uses `asyncio.run` or a thread when a loop is running.
|
|
61
|
+
- `asyncify(func)`: wrap a sync function so it can be awaited via `asyncio.to_thread`.
|
|
62
|
+
- `future(func)`: return a `Future` for the function execution; ensures there is a loop.
|
|
63
|
+
- `syncify_async_generator(async_gen)`: consume an async generator from sync code via a background thread.
|
|
64
|
+
- `auto_options_func(func)`: drop extra kwargs that the function does not accept.
|
|
65
|
+
|
|
66
|
+
When to use:
|
|
67
|
+
- Tool functions that may be sync or async.
|
|
68
|
+
- Adapters between streaming generators and sync APIs.
|
|
69
|
+
|
|
70
|
+
### GeneratorConsumer
|
|
71
|
+
Purpose: fan out a generator or async generator to multiple consumers, replay history, and handle errors.
|
|
72
|
+
|
|
73
|
+
Usage:
|
|
74
|
+
- Wrap a generator, then call `get_async_generator()` for multiple async consumers or `get_generator()` for sync.
|
|
75
|
+
- `get_result()` waits for completion and returns full history.
|
|
76
|
+
- `close()` cancels and notifies listeners.
|
|
77
|
+
|
|
78
|
+
When to use:
|
|
79
|
+
- Broadcast streaming output to multiple subscribers.
|
|
80
|
+
- Merge history + live updates reliably.
|
|
81
|
+
|
|
82
|
+
### LazyImport
|
|
83
|
+
Purpose: import optional deps and optionally auto-install via pip with version constraints.
|
|
84
|
+
|
|
85
|
+
Key methods:
|
|
86
|
+
- `from_import(from_package, target_modules, auto_install=True, version_constraint=None, install_name=None)`
|
|
87
|
+
- `import_package(package_name, auto_install=True, version_constraint=None, install_name=None)`
|
|
88
|
+
|
|
89
|
+
Notes:
|
|
90
|
+
- This prompts for installation in interactive use; plan for non-interactive runtime accordingly.
|
|
91
|
+
|
|
92
|
+
### Logger
|
|
93
|
+
Purpose: create a consistent logger with optional uvicorn integration.
|
|
94
|
+
|
|
95
|
+
Key pieces:
|
|
96
|
+
- `create_logger(app_name="Agently", log_level="INFO")` returns `AgentlyLogger` with `raise_error` helper.
|
|
97
|
+
|
|
98
|
+
### Messenger
|
|
99
|
+
Purpose: convenience wrapper for event center messaging.
|
|
100
|
+
|
|
101
|
+
Key method:
|
|
102
|
+
- `create_messenger(module_name)` delegates to `agently.base.event_center`.
|
|
103
|
+
|
|
104
|
+
### PythonSandbox
|
|
105
|
+
Purpose: execute small snippets safely with restricted builtins and whitelisted return types.
|
|
106
|
+
|
|
107
|
+
Key behaviors:
|
|
108
|
+
- `run(code)` executes in a sandbox; raises if a return type is not in `allowed_return_types`.
|
|
109
|
+
- `preset_objects` are wrapped to block private attributes and enforce safe return types.
|
|
110
|
+
|
|
111
|
+
When to use:
|
|
112
|
+
- Run short user-defined expressions or filters with safety checks.
|
|
113
|
+
|
|
114
|
+
### RuntimeData / RuntimeDataNamespace
|
|
115
|
+
Purpose: runtime-scoped hierarchical data with inheritance, dot-path access, and merge-friendly set semantics.
|
|
116
|
+
|
|
117
|
+
Key behaviors:
|
|
118
|
+
- `get(key, default=None, inherit=True)`: inherited view by default.
|
|
119
|
+
- `set` and `__setitem__` merge dict/list/set values rather than replace (unless you use `cover=True` internally).
|
|
120
|
+
- dot-path access: `data["a.b.c"]`.
|
|
121
|
+
- `namespace("path")` returns a namespace view.
|
|
122
|
+
- `dump("json"|"yaml"|"toml")`, `load(...)` support.
|
|
123
|
+
|
|
124
|
+
When to use:
|
|
125
|
+
- Store workflow state, memo, runtime configs.
|
|
126
|
+
|
|
127
|
+
### SerializableRuntimeData / SerializableRuntimeDataNamespace
|
|
128
|
+
Purpose: same API as `RuntimeData` but value types restricted to JSON-serializable shapes.
|
|
129
|
+
|
|
130
|
+
When to use:
|
|
131
|
+
- Settings and serialized runtime state.
|
|
132
|
+
|
|
133
|
+
### Settings / SettingsNamespace
|
|
134
|
+
Purpose: settings with mapping shortcuts and env substitution.
|
|
135
|
+
|
|
136
|
+
Key behaviors:
|
|
137
|
+
- `register_path_mappings("short", "actual.path")`: alias keys.
|
|
138
|
+
- `register_kv_mappings("key", "value", actual_settings)`: map a key+value to a settings dict.
|
|
139
|
+
- `set_settings(key, value, auto_load_env=False)`: apply mappings and optionally expand `${ENV.X}`.
|
|
140
|
+
|
|
141
|
+
When to use:
|
|
142
|
+
- Global or per-agent configuration with shortcuts.
|
|
143
|
+
|
|
144
|
+
### Storage / AsyncStorage
|
|
145
|
+
Purpose: simple SQLModel-based persistence with sync/async APIs.
|
|
146
|
+
|
|
147
|
+
Key behaviors:
|
|
148
|
+
- requires `sqlmodel`, `sqlalchemy`, `aiosqlite` via `LazyImport`.
|
|
149
|
+
- `set(obj|list)` merges into DB.
|
|
150
|
+
- `get(model, where=..., first=False, limit=..., offset=..., order_by=...)`.
|
|
151
|
+
- `create_tables()` calls `SQLModel.metadata.create_all`.
|
|
152
|
+
|
|
153
|
+
When to use:
|
|
154
|
+
- local state persistence for agents or tools.
|
|
155
|
+
|
|
156
|
+
### StreamingJSONCompleter
|
|
157
|
+
Purpose: complete partial JSON strings by closing open strings, comments, or brackets.
|
|
158
|
+
|
|
159
|
+
Key method:
|
|
160
|
+
- `append(data)` then `complete()` to get best-effort JSON.
|
|
161
|
+
|
|
162
|
+
When to use:
|
|
163
|
+
- streaming LLM output where JSON is partial.
|
|
164
|
+
|
|
165
|
+
### StreamingJSONParser
|
|
166
|
+
Purpose: parse streaming JSON and emit incremental updates (`delta`) and completion events (`done`).
|
|
167
|
+
|
|
168
|
+
Key behaviors:
|
|
169
|
+
- Uses `DataLocator` + `StreamingJSONCompleter` to find and parse JSON in noisy streams.
|
|
170
|
+
- Tracks schema order via `DataPathBuilder.extract_parsing_key_orders`.
|
|
171
|
+
- `parse_chunk(chunk)` yields `StreamingData` events.
|
|
172
|
+
- `parse_stream(chunk_stream)` yields events and finalizes at end.
|
|
173
|
+
|
|
174
|
+
When to use:
|
|
175
|
+
- UI streaming updates for structured output.
|
agently/utils/DataFormatter.py
CHANGED
|
@@ -56,15 +56,21 @@ class DataFormatter:
|
|
|
56
56
|
if issubclass(value, BaseModel):
|
|
57
57
|
extracted_value = {}
|
|
58
58
|
for name, field in value.model_fields.items():
|
|
59
|
+
annotation = field.annotation
|
|
60
|
+
if hasattr(field, "rebuild_annotation"):
|
|
61
|
+
try:
|
|
62
|
+
annotation = field.rebuild_annotation()
|
|
63
|
+
except Exception:
|
|
64
|
+
annotation = field.annotation
|
|
59
65
|
extracted_value.update(
|
|
60
66
|
{
|
|
61
67
|
name: (
|
|
62
68
|
(
|
|
63
|
-
DataFormatter.sanitize(
|
|
69
|
+
DataFormatter.sanitize(annotation, remain_type=remain_type),
|
|
64
70
|
field.description,
|
|
65
71
|
)
|
|
66
72
|
if field.description
|
|
67
|
-
else (DataFormatter.sanitize(
|
|
73
|
+
else (DataFormatter.sanitize(annotation, remain_type=remain_type),)
|
|
68
74
|
)
|
|
69
75
|
}
|
|
70
76
|
)
|
|
@@ -236,9 +242,11 @@ class DataFormatter:
|
|
|
236
242
|
|
|
237
243
|
if "additionalProperties" in input_schema:
|
|
238
244
|
additional_properties = input_schema["additionalProperties"]
|
|
239
|
-
if additional_properties is
|
|
245
|
+
if additional_properties is False:
|
|
246
|
+
pass
|
|
247
|
+
elif additional_properties is True or additional_properties is None:
|
|
240
248
|
kwargs_format["<*>"] = (Any, "")
|
|
241
|
-
|
|
249
|
+
elif isinstance(additional_properties, dict):
|
|
242
250
|
additional_type = additional_properties.pop("type", Any)
|
|
243
251
|
additional_properties.pop("title", None)
|
|
244
252
|
additional_desc = (
|
|
@@ -247,6 +255,8 @@ class DataFormatter:
|
|
|
247
255
|
else ""
|
|
248
256
|
)
|
|
249
257
|
kwargs_format["<*>"] = (additional_type, additional_desc)
|
|
258
|
+
else:
|
|
259
|
+
kwargs_format["<*>"] = (Any, "")
|
|
250
260
|
|
|
251
261
|
return kwargs_format or None
|
|
252
262
|
|
agently/utils/DataLocator.py
CHANGED
|
@@ -21,6 +21,101 @@ if TYPE_CHECKING:
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class DataLocator:
|
|
24
|
+
@staticmethod
|
|
25
|
+
def _locate_path_parts(
|
|
26
|
+
result: Any,
|
|
27
|
+
path_parts: list[str],
|
|
28
|
+
*,
|
|
29
|
+
style: Literal["dot", "slash"],
|
|
30
|
+
default: Any,
|
|
31
|
+
):
|
|
32
|
+
if not path_parts:
|
|
33
|
+
return result
|
|
34
|
+
path_part = path_parts[0]
|
|
35
|
+
remaining = path_parts[1:]
|
|
36
|
+
if style == "dot":
|
|
37
|
+
if "[" in path_part:
|
|
38
|
+
path_key_and_index = path_part.split("[")
|
|
39
|
+
path_key = path_key_and_index[0]
|
|
40
|
+
path_index = path_key_and_index[1][:-1]
|
|
41
|
+
if isinstance(result, Mapping):
|
|
42
|
+
result = result.get(path_key, default)
|
|
43
|
+
else:
|
|
44
|
+
return default
|
|
45
|
+
if path_index in ("*", ""):
|
|
46
|
+
if not isinstance(result, str) and isinstance(result, Sequence):
|
|
47
|
+
values = []
|
|
48
|
+
for item in result:
|
|
49
|
+
value = DataLocator._locate_path_parts(
|
|
50
|
+
item,
|
|
51
|
+
remaining,
|
|
52
|
+
style=style,
|
|
53
|
+
default=default,
|
|
54
|
+
)
|
|
55
|
+
if value is default:
|
|
56
|
+
return default
|
|
57
|
+
values.append(value)
|
|
58
|
+
return values
|
|
59
|
+
return default
|
|
60
|
+
try:
|
|
61
|
+
index = int(path_index)
|
|
62
|
+
except Exception:
|
|
63
|
+
return default
|
|
64
|
+
if not isinstance(result, str) and isinstance(result, Sequence):
|
|
65
|
+
try:
|
|
66
|
+
return DataLocator._locate_path_parts(
|
|
67
|
+
result[index],
|
|
68
|
+
remaining,
|
|
69
|
+
style=style,
|
|
70
|
+
default=default,
|
|
71
|
+
)
|
|
72
|
+
except Exception:
|
|
73
|
+
return default
|
|
74
|
+
return default
|
|
75
|
+
else:
|
|
76
|
+
if isinstance(result, Mapping):
|
|
77
|
+
return DataLocator._locate_path_parts(
|
|
78
|
+
result.get(path_part, default),
|
|
79
|
+
remaining,
|
|
80
|
+
style=style,
|
|
81
|
+
default=default,
|
|
82
|
+
)
|
|
83
|
+
return default
|
|
84
|
+
else:
|
|
85
|
+
if path_part == "*":
|
|
86
|
+
if not isinstance(result, str) and isinstance(result, Sequence):
|
|
87
|
+
values = []
|
|
88
|
+
for item in result:
|
|
89
|
+
value = DataLocator._locate_path_parts(
|
|
90
|
+
item,
|
|
91
|
+
remaining,
|
|
92
|
+
style=style,
|
|
93
|
+
default=default,
|
|
94
|
+
)
|
|
95
|
+
if value is default:
|
|
96
|
+
return default
|
|
97
|
+
values.append(value)
|
|
98
|
+
return values
|
|
99
|
+
return default
|
|
100
|
+
if isinstance(result, Mapping):
|
|
101
|
+
return DataLocator._locate_path_parts(
|
|
102
|
+
result.get(path_part, default),
|
|
103
|
+
remaining,
|
|
104
|
+
style=style,
|
|
105
|
+
default=default,
|
|
106
|
+
)
|
|
107
|
+
if not isinstance(result, str) and isinstance(result, Sequence):
|
|
108
|
+
try:
|
|
109
|
+
return DataLocator._locate_path_parts(
|
|
110
|
+
result[int(path_part)],
|
|
111
|
+
remaining,
|
|
112
|
+
style=style,
|
|
113
|
+
default=default,
|
|
114
|
+
)
|
|
115
|
+
except Exception:
|
|
116
|
+
return default
|
|
117
|
+
return default
|
|
118
|
+
|
|
24
119
|
@staticmethod
|
|
25
120
|
def locate_path_in_dict(
|
|
26
121
|
original_dict: dict,
|
|
@@ -34,42 +129,24 @@ class DataLocator:
|
|
|
34
129
|
match style:
|
|
35
130
|
case "dot":
|
|
36
131
|
try:
|
|
37
|
-
result = original_dict
|
|
38
132
|
path_parts = path.split(".")
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
result = result[path_key]
|
|
46
|
-
else:
|
|
47
|
-
return default
|
|
48
|
-
if not isinstance(result, str) and isinstance(result, Sequence):
|
|
49
|
-
result = result[path_index]
|
|
50
|
-
else:
|
|
51
|
-
return default
|
|
52
|
-
else:
|
|
53
|
-
if isinstance(result, Mapping):
|
|
54
|
-
result = result[path_part]
|
|
55
|
-
else:
|
|
56
|
-
return default
|
|
57
|
-
return result
|
|
133
|
+
return DataLocator._locate_path_parts(
|
|
134
|
+
original_dict,
|
|
135
|
+
path_parts,
|
|
136
|
+
style="dot",
|
|
137
|
+
default=default,
|
|
138
|
+
)
|
|
58
139
|
except Exception:
|
|
59
140
|
return default
|
|
60
141
|
case "slash":
|
|
61
|
-
result = original_dict
|
|
62
|
-
path_parts = path.split("/")
|
|
63
142
|
try:
|
|
64
|
-
for
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return default
|
|
72
|
-
return result
|
|
143
|
+
path_parts = [part for part in path.split("/") if part]
|
|
144
|
+
return DataLocator._locate_path_parts(
|
|
145
|
+
original_dict,
|
|
146
|
+
path_parts,
|
|
147
|
+
style="slash",
|
|
148
|
+
default=default,
|
|
149
|
+
)
|
|
73
150
|
except Exception:
|
|
74
151
|
return default
|
|
75
152
|
|
agently/utils/FunctionShifter.py
CHANGED
|
@@ -76,8 +76,9 @@ class FunctionShifter:
|
|
|
76
76
|
|
|
77
77
|
@wraps(func)
|
|
78
78
|
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
if not callable(func):
|
|
80
|
+
raise TypeError(f"Expected a callable, got {type(func)}")
|
|
81
|
+
return await asyncio.to_thread(func, *args, **kwargs) # type: ignore
|
|
81
82
|
|
|
82
83
|
return wrapper
|
|
83
84
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Copyright 2023-2025 AgentEra(Agently.Tech)
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TimeInfo:
|
|
20
|
+
@staticmethod
|
|
21
|
+
def get_current_time():
|
|
22
|
+
return datetime.now().strftime("%Y-%m-%d %H:%M:%S %A")
|
agently/utils/__init__.py
CHANGED