flatagents 4.1.0__tar.gz → 4.2.1__tar.gz
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.
- {flatagents-4.1.0 → flatagents-4.2.1}/AGENTS.md +1 -1
- {flatagents-4.1.0 → flatagents-4.2.1}/PKG-INFO +2 -2
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/__init__.py +1 -1
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/adapters/claude_code.py +6 -1
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/adapters/codex_cli.py +27 -5
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/adapters/pi_agent_bridge.py +6 -2
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/adapters/smolagents.py +3 -1
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/assets/flatagent.d.ts +1 -1
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/assets/flatagent.slim.d.ts +1 -1
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/assets/flatagents-runtime.d.ts +1 -1
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/assets/flatagents-runtime.schema.json +1 -1
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/assets/flatagents-runtime.slim.d.ts +1 -1
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/assets/flatmachine.d.ts +4 -1
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/assets/flatmachine.schema.json +6 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/assets/flatmachine.slim.d.ts +4 -1
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/assets/profile.d.ts +1 -1
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/assets/profile.slim.d.ts +1 -1
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/assets/prompt.d.ts +1 -1
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/assets/prompt.slim.d.ts +1 -1
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/baseagent.py +1 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/flatagent.py +10 -3
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/monitoring.py +22 -11
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/profiles.py +15 -4
- flatagents-4.2.1/flatagents/tests/__init__.py +0 -0
- flatagents-4.2.1/flatagents/tests/test_monitoring.py +189 -0
- flatagents-4.2.1/flatagents/tests/test_profiles.py +25 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/pyproject.toml +2 -2
- {flatagents-4.1.0 → flatagents-4.2.1}/.gitignore +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/README.md +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/adapters/__init__.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/adapters/call_throttle.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/adapters/claude_code_sessions.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/adapters/codex_cli_sessions.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/adapters/compat.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/adapters/pi_agent_runner.mjs +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/assets/README.md +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/assets/__init__.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/assets/flatagent.schema.json +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/assets/profile.schema.json +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/assets/prompt.schema.json +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/providers/__init__.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/providers/anthropic.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/providers/cerebras.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/providers/github_copilot_auth.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/providers/github_copilot_client.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/providers/github_copilot_login.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/providers/github_copilot_types.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/providers/openai.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/providers/openai_codex_auth.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/providers/openai_codex_client.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/providers/openai_codex_login.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/providers/openai_codex_types.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/tool_loop.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/tools.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/utils.py +0 -0
- {flatagents-4.1.0 → flatagents-4.2.1}/flatagents/validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flatagents
|
|
3
|
-
Version: 4.1
|
|
3
|
+
Version: 4.2.1
|
|
4
4
|
Summary: A lightweight framework for building LLM-powered agents.
|
|
5
5
|
Project-URL: Homepage, https://github.com/memgrafter/flatagents
|
|
6
6
|
Project-URL: Repository, https://github.com/memgrafter/flatagents
|
|
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
-
Requires-Python: >=3.
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
21
|
Requires-Dist: aiofiles
|
|
22
22
|
Requires-Dist: httpx
|
|
23
23
|
Requires-Dist: jinja2
|
|
@@ -838,7 +838,12 @@ class ClaudeCodeExecutor:
|
|
|
838
838
|
# MCP server configuration
|
|
839
839
|
mcp_config = cfg.get("mcp_config")
|
|
840
840
|
if mcp_config:
|
|
841
|
-
|
|
841
|
+
mcp_config_path = os.path.expanduser(str(mcp_config))
|
|
842
|
+
if not os.path.isabs(mcp_config_path):
|
|
843
|
+
mcp_config_path = os.path.join(self._config_dir, mcp_config_path)
|
|
844
|
+
if not os.path.isfile(mcp_config_path):
|
|
845
|
+
raise FileNotFoundError(f"Claude MCP config file not found: {mcp_config_path}")
|
|
846
|
+
args += ["--mcp-config", mcp_config_path]
|
|
842
847
|
|
|
843
848
|
# Budget (0 = disabled)
|
|
844
849
|
max_budget = cfg.get("max_budget_usd", 0)
|
|
@@ -40,6 +40,17 @@ Config keys (agent config or global settings.agent_runners.codex_cli):
|
|
|
40
40
|
rate_limit_jitter +/-seconds jitter (default: 0)
|
|
41
41
|
use_app_server bool -- use app-server transport (required for fork)
|
|
42
42
|
session_source App-server session source tag (default: "exec")
|
|
43
|
+
|
|
44
|
+
Session semantics:
|
|
45
|
+
- FlatMachine ``state.session_id`` is accepted by execute() for executor
|
|
46
|
+
protocol compatibility only. It is a machine-level cache/session key and
|
|
47
|
+
is NOT a Codex thread id.
|
|
48
|
+
- To resume an existing Codex CLI conversation/thread, pass
|
|
49
|
+
``input.resume_session``. Exec transport maps it to
|
|
50
|
+
``codex exec resume <SESSION_ID>``; app-server transport maps it to
|
|
51
|
+
``thread/resume``.
|
|
52
|
+
- Do not map arbitrary ``state.session_id`` values to Codex resume; Codex
|
|
53
|
+
requires the real thread id returned in ``output.thread_id``.
|
|
43
54
|
"""
|
|
44
55
|
|
|
45
56
|
from __future__ import annotations
|
|
@@ -384,8 +395,9 @@ class CodexCliExecutor:
|
|
|
384
395
|
"""Execute a Codex CLI invocation.
|
|
385
396
|
|
|
386
397
|
`session_id` is accepted for AgentExecutor protocol compatibility.
|
|
387
|
-
|
|
388
|
-
|
|
398
|
+
It is intentionally ignored because FlatMachine state session ids are
|
|
399
|
+
not Codex thread ids. To resume a Codex CLI thread, pass the real
|
|
400
|
+
Codex thread id as `input.resume_session`.
|
|
389
401
|
"""
|
|
390
402
|
task = input_data.get("task") or input_data.get("prompt", "")
|
|
391
403
|
if not task:
|
|
@@ -641,7 +653,12 @@ class CodexCliExecutor:
|
|
|
641
653
|
# Output schema
|
|
642
654
|
output_schema = cfg.get("output_schema")
|
|
643
655
|
if output_schema:
|
|
644
|
-
|
|
656
|
+
output_schema_path = os.path.expanduser(str(output_schema))
|
|
657
|
+
if not os.path.isabs(output_schema_path):
|
|
658
|
+
output_schema_path = os.path.join(self._config_dir, output_schema_path)
|
|
659
|
+
if not os.path.isfile(output_schema_path):
|
|
660
|
+
raise FileNotFoundError(f"Codex output schema file not found: {output_schema_path}")
|
|
661
|
+
args += ["--output-schema", output_schema_path]
|
|
645
662
|
|
|
646
663
|
# Additional directories
|
|
647
664
|
add_dirs = cfg.get("add_dirs")
|
|
@@ -862,8 +879,13 @@ class CodexCliExecutor:
|
|
|
862
879
|
# Load output schema if configured
|
|
863
880
|
output_schema = None
|
|
864
881
|
schema_path = self._merged.get("output_schema")
|
|
865
|
-
if schema_path
|
|
866
|
-
|
|
882
|
+
if schema_path:
|
|
883
|
+
schema_path = os.path.expanduser(str(schema_path))
|
|
884
|
+
if not os.path.isabs(schema_path):
|
|
885
|
+
schema_path = os.path.join(self._config_dir, schema_path)
|
|
886
|
+
if not os.path.isfile(schema_path):
|
|
887
|
+
raise FileNotFoundError(f"Codex output schema file not found: {schema_path}")
|
|
888
|
+
with open(schema_path) as f:
|
|
867
889
|
output_schema = json.load(f)
|
|
868
890
|
|
|
869
891
|
await transport.turn_start(thread_id, text, output_schema)
|
|
@@ -103,8 +103,12 @@ def create_pi_agent_bridge_executor(
|
|
|
103
103
|
runner_path = config.get("runner") or settings.get("runner")
|
|
104
104
|
if not runner_path:
|
|
105
105
|
runner_path = os.path.join(os.path.dirname(__file__), "pi_agent_runner.mjs")
|
|
106
|
-
|
|
107
|
-
runner_path = os.path.
|
|
106
|
+
else:
|
|
107
|
+
runner_path = os.path.expanduser(runner_path)
|
|
108
|
+
if not os.path.isabs(runner_path):
|
|
109
|
+
runner_path = os.path.join(config_dir, runner_path)
|
|
110
|
+
if not os.path.isfile(runner_path):
|
|
111
|
+
raise FileNotFoundError(f"pi-agent runner file not found: {runner_path}")
|
|
108
112
|
|
|
109
113
|
node_path = config.get("node") or settings.get("node") or "node"
|
|
110
114
|
timeout = config.get("timeout") or settings.get("timeout")
|
|
@@ -105,9 +105,11 @@ def _load_factory(ref: str, config_dir: str):
|
|
|
105
105
|
module_ref, factory_name = _parse_ref(ref)
|
|
106
106
|
|
|
107
107
|
if module_ref.endswith(".py") or module_ref.startswith(".") or "/" in module_ref:
|
|
108
|
-
module_path = module_ref
|
|
108
|
+
module_path = os.path.expanduser(module_ref)
|
|
109
109
|
if not os.path.isabs(module_path):
|
|
110
110
|
module_path = os.path.join(config_dir, module_path)
|
|
111
|
+
if not os.path.isfile(module_path):
|
|
112
|
+
raise FileNotFoundError(f"Smolagents factory file not found: {module_path}")
|
|
111
113
|
spec = importlib.util.spec_from_file_location("smolagents_factory", module_path)
|
|
112
114
|
if spec is None or spec.loader is None:
|
|
113
115
|
raise ImportError(f"Unable to load smolagents factory from {module_path}")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const SPEC_VERSION = "4.1
|
|
1
|
+
export const SPEC_VERSION = "4.2.1";
|
|
2
2
|
import { PromptWrapper, PromptData, PromptRef, OutputSchema, MCPConfig, ToolDefinition, } from "./prompt";
|
|
3
3
|
import { ProfileWrapper, ProfileData, ProfileRef, ModelConfig, OAuthConfig, } from "./profile";
|
|
4
4
|
export interface AgentWrapper {
|
|
@@ -252,7 +252,7 @@ export interface BackendConfig {
|
|
|
252
252
|
dynamodb_table?: string;
|
|
253
253
|
aws_region?: string;
|
|
254
254
|
}
|
|
255
|
-
export const SPEC_VERSION = "4.1
|
|
255
|
+
export const SPEC_VERSION = "4.2.1";
|
|
256
256
|
export interface SDKRuntimeWrapper {
|
|
257
257
|
spec: "flatagents-runtime";
|
|
258
258
|
spec_version: typeof SPEC_VERSION;
|
|
@@ -190,7 +190,7 @@ waiting_channel - Signal channel this machine is blocked on (v1.2.0)
|
|
|
190
190
|
config_hash - Content-addressed machine config key for cross-SDK resume (v2.1.0)
|
|
191
191
|
*/
|
|
192
192
|
|
|
193
|
-
export const SPEC_VERSION = "4.1
|
|
193
|
+
export const SPEC_VERSION = "4.2.1";
|
|
194
194
|
|
|
195
195
|
export interface MachineWrapper {
|
|
196
196
|
spec: "flatmachine";
|
|
@@ -209,6 +209,7 @@ export interface MachineRuntimeMetadata {
|
|
|
209
209
|
current_state: string;
|
|
210
210
|
total_api_calls: number;
|
|
211
211
|
total_cost: number;
|
|
212
|
+
depth?: number;
|
|
212
213
|
}
|
|
213
214
|
|
|
214
215
|
export interface MachineData {
|
|
@@ -245,6 +246,7 @@ export interface HooksRefConfig {
|
|
|
245
246
|
export interface MachineSettings {
|
|
246
247
|
max_steps?: number;
|
|
247
248
|
parallel_fallback?: "sequential" | "error";
|
|
249
|
+
max_depth?: number;
|
|
248
250
|
[key: string]: any;
|
|
249
251
|
}
|
|
250
252
|
|
|
@@ -350,6 +352,7 @@ export interface MachineSnapshot {
|
|
|
350
352
|
loop_cost: number;
|
|
351
353
|
};
|
|
352
354
|
config_hash?: string;
|
|
355
|
+
depth?: number;
|
|
353
356
|
}
|
|
354
357
|
|
|
355
358
|
export interface PersistenceConfig {
|
|
@@ -114,6 +114,9 @@
|
|
|
114
114
|
},
|
|
115
115
|
"total_cost": {
|
|
116
116
|
"type": "number"
|
|
117
|
+
},
|
|
118
|
+
"depth": {
|
|
119
|
+
"type": "number"
|
|
117
120
|
}
|
|
118
121
|
},
|
|
119
122
|
"required": [
|
|
@@ -1127,6 +1130,9 @@
|
|
|
1127
1130
|
"sequential",
|
|
1128
1131
|
"error"
|
|
1129
1132
|
]
|
|
1133
|
+
},
|
|
1134
|
+
"max_depth": {
|
|
1135
|
+
"type": "number"
|
|
1130
1136
|
}
|
|
1131
1137
|
}
|
|
1132
1138
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const SPEC_VERSION = "4.1
|
|
1
|
+
export const SPEC_VERSION = "4.2.1";
|
|
2
2
|
export interface MachineWrapper {
|
|
3
3
|
spec: "flatmachine";
|
|
4
4
|
spec_version: string;
|
|
@@ -14,6 +14,7 @@ export interface MachineRuntimeMetadata {
|
|
|
14
14
|
current_state: string;
|
|
15
15
|
total_api_calls: number;
|
|
16
16
|
total_cost: number;
|
|
17
|
+
depth?: number;
|
|
17
18
|
}
|
|
18
19
|
export interface MachineData {
|
|
19
20
|
name?: string;
|
|
@@ -42,6 +43,7 @@ export interface HooksRefConfig {
|
|
|
42
43
|
export interface MachineSettings {
|
|
43
44
|
max_steps?: number;
|
|
44
45
|
parallel_fallback?: "sequential" | "error";
|
|
46
|
+
max_depth?: number;
|
|
45
47
|
[key: string]: any;
|
|
46
48
|
}
|
|
47
49
|
export interface StateDefinition {
|
|
@@ -125,6 +127,7 @@ export interface MachineSnapshot {
|
|
|
125
127
|
loop_cost: number;
|
|
126
128
|
};
|
|
127
129
|
config_hash?: string;
|
|
130
|
+
depth?: number;
|
|
128
131
|
}
|
|
129
132
|
export interface PersistenceConfig {
|
|
130
133
|
enabled: boolean;
|
|
@@ -1030,6 +1030,7 @@ class FlatAgent(ABC):
|
|
|
1030
1030
|
config = {}
|
|
1031
1031
|
|
|
1032
1032
|
if config_file is not None:
|
|
1033
|
+
config_file = os.path.abspath(os.path.expanduser(config_file))
|
|
1033
1034
|
if not os.path.exists(config_file):
|
|
1034
1035
|
raise FileNotFoundError(f"Configuration file not found: {config_file}")
|
|
1035
1036
|
|
|
@@ -446,6 +446,7 @@ class FlatAgent:
|
|
|
446
446
|
config_dir = kwargs.get("config_dir") or os.getcwd()
|
|
447
447
|
|
|
448
448
|
if config_file is not None:
|
|
449
|
+
config_file = os.path.abspath(os.path.expanduser(config_file))
|
|
449
450
|
if not os.path.exists(config_file):
|
|
450
451
|
raise FileNotFoundError(f"Configuration file not found: {config_file}")
|
|
451
452
|
with open(config_file, 'r') as f:
|
|
@@ -490,7 +491,7 @@ class FlatAgent:
|
|
|
490
491
|
"""Apply resolved llm model config to instance attributes."""
|
|
491
492
|
provider = model_config.get('provider')
|
|
492
493
|
model_name = model_config.get('name')
|
|
493
|
-
if provider and model_name and
|
|
494
|
+
if provider and model_name and f"{provider}/" not in str(model_name) and f"{provider}:" not in str(model_name):
|
|
494
495
|
full_model_name = f"{provider}/{model_name}"
|
|
495
496
|
else:
|
|
496
497
|
full_model_name = model_name
|
|
@@ -515,7 +516,8 @@ class FlatAgent:
|
|
|
515
516
|
except ImportError:
|
|
516
517
|
yaml = None
|
|
517
518
|
|
|
518
|
-
|
|
519
|
+
expanded = os.path.expanduser(ref)
|
|
520
|
+
path = expanded if os.path.isabs(expanded) else os.path.join(self._config_dir, expanded)
|
|
519
521
|
if not os.path.exists(path):
|
|
520
522
|
raise FileNotFoundError(f"Referenced config file not found: {path}")
|
|
521
523
|
|
|
@@ -921,9 +923,12 @@ class FlatAgent:
|
|
|
921
923
|
input_data: Dict[str, Any],
|
|
922
924
|
tools_prompt: str = "",
|
|
923
925
|
tools: Optional[List[Dict]] = None,
|
|
926
|
+
context: Optional[Dict[str, Any]] = None,
|
|
924
927
|
) -> str:
|
|
925
928
|
"""
|
|
926
|
-
Render post-history instructions with the same template context as prompts
|
|
929
|
+
Render post-history instructions with the same template context as prompts,
|
|
930
|
+
plus an additional ``context`` variable for data that persists across turns
|
|
931
|
+
(e.g. role state maintained by a FlatMachine tool loop).
|
|
927
932
|
|
|
928
933
|
These instructions are not part of the stored logical conversation. They
|
|
929
934
|
are appended ephemerally to the final submitted user/tool message after
|
|
@@ -949,6 +954,7 @@ class FlatAgent:
|
|
|
949
954
|
tools_prompt=tools_prompt,
|
|
950
955
|
tools=tools or [],
|
|
951
956
|
model=model_config,
|
|
957
|
+
context=context or {},
|
|
952
958
|
)
|
|
953
959
|
|
|
954
960
|
def _messages_with_post_history_instructions(
|
|
@@ -1250,6 +1256,7 @@ class FlatAgent:
|
|
|
1250
1256
|
input_data,
|
|
1251
1257
|
tools_prompt=tools_prompt,
|
|
1252
1258
|
tools=_mcp_tools,
|
|
1259
|
+
context=context,
|
|
1253
1260
|
)
|
|
1254
1261
|
submit_messages = self._messages_with_post_history_instructions(
|
|
1255
1262
|
all_messages,
|
|
@@ -120,15 +120,21 @@ def setup_logging(
|
|
|
120
120
|
if force:
|
|
121
121
|
lib_logger.handlers.clear()
|
|
122
122
|
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
123
|
+
# Opt-in to a console handler via FLATAGENTS_LOG_HANDLER.
|
|
124
|
+
# This keeps headful apps (CLI, TUI, REPL) from seeing library
|
|
125
|
+
# logs unless the user explicitly requests them.
|
|
126
|
+
add_console = os.getenv('FLATAGENTS_LOG_HANDLER', '').lower() in ('stdout', 'console', 'true')
|
|
127
|
+
|
|
128
|
+
if add_console:
|
|
129
|
+
if not lib_logger.handlers:
|
|
130
|
+
stdout_handler = logging.StreamHandler(sys.stdout)
|
|
131
|
+
stdout_handler.setFormatter(formatter)
|
|
132
|
+
lib_logger.addHandler(stdout_handler)
|
|
133
|
+
lib_logger.propagate = False
|
|
134
|
+
else:
|
|
135
|
+
# No stdout handler by default — logs propagate to the host app's
|
|
136
|
+
# root logger so headful apps control whether they appear on console.
|
|
137
|
+
lib_logger.propagate = True
|
|
132
138
|
|
|
133
139
|
# Add file handler if FLATAGENTS_LOG_DIR is set
|
|
134
140
|
log_dir = os.getenv('FLATAGENTS_LOG_DIR')
|
|
@@ -232,8 +238,13 @@ def _init_metrics() -> None:
|
|
|
232
238
|
SERVICE_NAME: service_name
|
|
233
239
|
})
|
|
234
240
|
|
|
235
|
-
# Check which exporter to use (default to
|
|
236
|
-
|
|
241
|
+
# Check which exporter to use (default to none — headful apps
|
|
242
|
+
# control whether metrics appear via explicit env opt-in).
|
|
243
|
+
exporter_type = os.getenv('OTEL_METRICS_EXPORTER', 'none').lower()
|
|
244
|
+
|
|
245
|
+
if exporter_type == 'none':
|
|
246
|
+
_metrics_enabled = False
|
|
247
|
+
return
|
|
237
248
|
|
|
238
249
|
if exporter_type == 'console':
|
|
239
250
|
# Use console exporter for testing/debugging
|
|
@@ -196,7 +196,12 @@ class ProfileManager:
|
|
|
196
196
|
if override_cfg:
|
|
197
197
|
result.update(override_cfg)
|
|
198
198
|
else:
|
|
199
|
-
|
|
199
|
+
available_profiles = list(self._profiles.keys())
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"Override profile '{self._override_profile}' not found. "
|
|
202
|
+
f"The intended model override was NOT applied. "
|
|
203
|
+
f"Available profiles: {available_profiles}"
|
|
204
|
+
)
|
|
200
205
|
|
|
201
206
|
return result
|
|
202
207
|
|
|
@@ -224,9 +229,9 @@ def load_profiles_from_file(profiles_file: str) -> Dict[str, Any]:
|
|
|
224
229
|
except ImportError:
|
|
225
230
|
raise ImportError("pyyaml is required for profiles.yml")
|
|
226
231
|
|
|
232
|
+
profiles_file = os.path.abspath(os.path.expanduser(profiles_file))
|
|
227
233
|
if not os.path.exists(profiles_file):
|
|
228
|
-
|
|
229
|
-
return {'profiles': {}, 'default': None, 'override': None}
|
|
234
|
+
raise FileNotFoundError(f"Profiles file not found: {profiles_file}")
|
|
230
235
|
|
|
231
236
|
with open(profiles_file, 'r') as f:
|
|
232
237
|
config = yaml.safe_load(f) or {}
|
|
@@ -320,7 +325,13 @@ def discover_profiles_file(config_dir: str, explicit_path: Optional[str] = None)
|
|
|
320
325
|
Path to profiles.yml if found, explicit_path if provided, or None
|
|
321
326
|
"""
|
|
322
327
|
if explicit_path:
|
|
323
|
-
|
|
328
|
+
path = os.path.expanduser(explicit_path)
|
|
329
|
+
if not os.path.isabs(path):
|
|
330
|
+
path = os.path.join(config_dir, path)
|
|
331
|
+
path = os.path.abspath(path)
|
|
332
|
+
if not os.path.exists(path):
|
|
333
|
+
raise FileNotFoundError(f"Profiles file not found: {path}")
|
|
334
|
+
return path
|
|
324
335
|
default_path = os.path.join(config_dir, 'profiles.yml')
|
|
325
336
|
return default_path if os.path.exists(default_path) else None
|
|
326
337
|
|
|
File without changes
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Tests for monitoring.py — headful logging opt-in and metrics defaults."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
import importlib
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# ── Helpers ──────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
_ENV_KEYS = (
|
|
13
|
+
"FLATAGENTS_LOG_HANDLER",
|
|
14
|
+
"FLATAGENTS_LOG_LEVEL",
|
|
15
|
+
"FLATAGENTS_LOG_FORMAT",
|
|
16
|
+
"FLATAGENTS_LOG_DIR",
|
|
17
|
+
"FLATAGENTS_METRICS_ENABLED",
|
|
18
|
+
"OTEL_METRICS_EXPORTER",
|
|
19
|
+
"OTEL_SERVICE_NAME",
|
|
20
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _reset_env(monkeypatch):
|
|
25
|
+
"""Clear flatagents-related env vars."""
|
|
26
|
+
for key in _ENV_KEYS:
|
|
27
|
+
monkeypatch.delenv(key, raising=False)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _reset_module(mod):
|
|
31
|
+
"""Reset flatagents.monitoring internal state."""
|
|
32
|
+
mod._logging_configured = False
|
|
33
|
+
mod._metrics_init_attempted = False
|
|
34
|
+
mod._metrics_enabled = False
|
|
35
|
+
mod._meter = None
|
|
36
|
+
mod._cached_histograms.clear()
|
|
37
|
+
lib_logger = logging.getLogger("flatagents")
|
|
38
|
+
lib_logger.handlers.clear()
|
|
39
|
+
lib_logger.propagate = True
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ── Fixtures ────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.fixture(autouse=True)
|
|
46
|
+
def _clean_flatagents(monkeypatch):
|
|
47
|
+
"""Auto-reset env and module state for every test."""
|
|
48
|
+
_reset_env(monkeypatch)
|
|
49
|
+
mod = importlib.import_module("flatagents.monitoring")
|
|
50
|
+
_reset_module(mod)
|
|
51
|
+
yield
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ── Issue 1: setup_logging ──────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestSetupLoggingDefault:
|
|
58
|
+
"""setup_logging() adds no stdout handler and sets propagate=True."""
|
|
59
|
+
|
|
60
|
+
def test_no_console_handler_by_default(self):
|
|
61
|
+
from flatagents.monitoring import setup_logging
|
|
62
|
+
|
|
63
|
+
setup_logging()
|
|
64
|
+
lib_logger = logging.getLogger("flatagents")
|
|
65
|
+
|
|
66
|
+
stdout_handlers = [
|
|
67
|
+
h
|
|
68
|
+
for h in lib_logger.handlers
|
|
69
|
+
if isinstance(h, logging.StreamHandler)
|
|
70
|
+
and h.stream is sys.stdout
|
|
71
|
+
]
|
|
72
|
+
assert len(stdout_handlers) == 0
|
|
73
|
+
|
|
74
|
+
def test_propagate_is_true_by_default(self):
|
|
75
|
+
from flatagents.monitoring import setup_logging
|
|
76
|
+
|
|
77
|
+
setup_logging()
|
|
78
|
+
lib_logger = logging.getLogger("flatagents")
|
|
79
|
+
|
|
80
|
+
assert lib_logger.propagate is True
|
|
81
|
+
|
|
82
|
+
def test_no_duplicate_handlers_on_second_call(self):
|
|
83
|
+
from flatagents.monitoring import setup_logging
|
|
84
|
+
|
|
85
|
+
setup_logging()
|
|
86
|
+
setup_logging()
|
|
87
|
+
lib_logger = logging.getLogger("flatagents")
|
|
88
|
+
|
|
89
|
+
stdout_handlers = [
|
|
90
|
+
h
|
|
91
|
+
for h in lib_logger.handlers
|
|
92
|
+
if isinstance(h, logging.StreamHandler)
|
|
93
|
+
and h.stream is sys.stdout
|
|
94
|
+
]
|
|
95
|
+
assert len(stdout_handlers) <= 1
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TestSetupLoggingOptIn:
|
|
99
|
+
"""FLATAGENTS_LOG_HANDLER=stdout adds handler and sets propagate=False."""
|
|
100
|
+
|
|
101
|
+
@pytest.mark.parametrize(
|
|
102
|
+
"opt_in_value, should_add_handler",
|
|
103
|
+
[
|
|
104
|
+
("stdout", True),
|
|
105
|
+
("console", True),
|
|
106
|
+
("true", True),
|
|
107
|
+
("STDLONG", False),
|
|
108
|
+
("1", False),
|
|
109
|
+
("", False),
|
|
110
|
+
],
|
|
111
|
+
)
|
|
112
|
+
def test_opt_in_variations(self, opt_in_value, should_add_handler, monkeypatch):
|
|
113
|
+
"""Test various env values for FLATAGENTS_LOG_HANDLER."""
|
|
114
|
+
if opt_in_value:
|
|
115
|
+
monkeypatch.setenv("FLATAGENTS_LOG_HANDLER", opt_in_value)
|
|
116
|
+
|
|
117
|
+
from flatagents.monitoring import setup_logging
|
|
118
|
+
|
|
119
|
+
setup_logging()
|
|
120
|
+
lib_logger = logging.getLogger("flatagents")
|
|
121
|
+
|
|
122
|
+
stdout_handlers = [
|
|
123
|
+
h
|
|
124
|
+
for h in lib_logger.handlers
|
|
125
|
+
if isinstance(h, logging.StreamHandler)
|
|
126
|
+
and h.stream is sys.stdout
|
|
127
|
+
]
|
|
128
|
+
assert len(stdout_handlers) == (1 if should_add_handler else 0)
|
|
129
|
+
# When handler added: propagate=False; when not: propagate=True
|
|
130
|
+
assert lib_logger.propagate == (not should_add_handler)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ── Issue 2: _init_metrics ─────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
class TestInitMetricsDefault:
|
|
136
|
+
"""_init_metrics() with default env does NOT create console exporter."""
|
|
137
|
+
|
|
138
|
+
def test_metrics_disabled_by_default(self):
|
|
139
|
+
"""Even when otel is installed, default exporter=none disables metrics."""
|
|
140
|
+
try:
|
|
141
|
+
import opentelemetry # noqa: F401
|
|
142
|
+
except ImportError:
|
|
143
|
+
pytest.skip("opentelemetry not installed")
|
|
144
|
+
|
|
145
|
+
import flatagents.monitoring as mod
|
|
146
|
+
from flatagents.monitoring import _init_metrics
|
|
147
|
+
|
|
148
|
+
_init_metrics()
|
|
149
|
+
assert mod._metrics_enabled is False
|
|
150
|
+
|
|
151
|
+
def test_get_meter_returns_none_when_metrics_disabled(self):
|
|
152
|
+
try:
|
|
153
|
+
import opentelemetry # noqa: F401
|
|
154
|
+
except ImportError:
|
|
155
|
+
pytest.skip("opentelemetry not installed")
|
|
156
|
+
|
|
157
|
+
from flatagents.monitoring import get_meter
|
|
158
|
+
|
|
159
|
+
assert get_meter() is None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class TestInitMetricsConsoleExporter:
|
|
163
|
+
"""OTEL_METRICS_EXPORTER=console creates a console exporter."""
|
|
164
|
+
|
|
165
|
+
def test_metrics_enabled_with_console_exporter(self, monkeypatch):
|
|
166
|
+
try:
|
|
167
|
+
import opentelemetry # noqa: F401
|
|
168
|
+
except ImportError:
|
|
169
|
+
pytest.skip("opentelemetry not installed")
|
|
170
|
+
|
|
171
|
+
monkeypatch.setenv("OTEL_METRICS_EXPORTER", "console")
|
|
172
|
+
|
|
173
|
+
import flatagents.monitoring as mod
|
|
174
|
+
from flatagents.monitoring import _init_metrics
|
|
175
|
+
|
|
176
|
+
_init_metrics()
|
|
177
|
+
assert mod._metrics_enabled is True
|
|
178
|
+
|
|
179
|
+
def test_get_meter_returns_meter_with_console_exporter(self, monkeypatch):
|
|
180
|
+
try:
|
|
181
|
+
import opentelemetry # noqa: F401
|
|
182
|
+
except ImportError:
|
|
183
|
+
pytest.skip("opentelemetry not installed")
|
|
184
|
+
|
|
185
|
+
monkeypatch.setenv("OTEL_METRICS_EXPORTER", "console")
|
|
186
|
+
|
|
187
|
+
from flatagents.monitoring import get_meter
|
|
188
|
+
|
|
189
|
+
assert get_meter() is not None
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Tests for model profile resolution."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from flatagents.profiles import ProfileManager
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_missing_override_profile_fails_fast_with_actionable_message():
|
|
9
|
+
manager = ProfileManager(
|
|
10
|
+
{
|
|
11
|
+
"profiles": {
|
|
12
|
+
"cheap": {"provider": "cerebras", "name": "zai-glm-4.6"},
|
|
13
|
+
},
|
|
14
|
+
"default": "cheap",
|
|
15
|
+
"override": "fast",
|
|
16
|
+
}
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
with pytest.raises(ValueError) as exc_info:
|
|
20
|
+
manager.resolve_model_config(None)
|
|
21
|
+
|
|
22
|
+
message = str(exc_info.value)
|
|
23
|
+
assert "Override profile 'fast' not found" in message
|
|
24
|
+
assert "intended model override was NOT applied" in message
|
|
25
|
+
assert "cheap" in message
|
|
@@ -4,11 +4,11 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "flatagents"
|
|
7
|
-
version = "4.1
|
|
7
|
+
version = "4.2.1"
|
|
8
8
|
description = "A lightweight framework for building LLM-powered agents."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
11
|
-
requires-python = ">=3.
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
12
|
authors = [
|
|
13
13
|
{ email = "memgrafter@gmail.com" }
|
|
14
14
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|