flowent 0.2.3 → 0.3.0
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.
- package/README.md +3 -3
- package/backend/README.md +3 -3
- package/backend/pyproject.toml +1 -1
- package/backend/src/flowent/agent.py +1 -1
- package/backend/src/flowent/api_models.py +103 -0
- package/backend/src/flowent/app.py +151 -0
- package/backend/src/flowent/cli.py +13 -4
- package/backend/src/flowent/compact.py +34 -13
- package/backend/src/flowent/llm.py +6 -8
- package/backend/src/flowent/logging.py +7 -1
- package/backend/src/flowent/main.py +18 -1989
- package/backend/src/flowent/mcp.py +231 -44
- package/backend/src/flowent/network.py +5 -0
- package/backend/src/flowent/permissions.py +5 -1
- package/backend/src/flowent/provider_connections.py +42 -0
- package/backend/src/flowent/routes/__init__.py +0 -0
- package/backend/src/flowent/routes/integrations.py +105 -0
- package/backend/src/flowent/routes/permissions.py +36 -0
- package/backend/src/flowent/routes/providers.py +30 -0
- package/backend/src/flowent/routes/system.py +49 -0
- package/backend/src/flowent/routes/workflow_routes.py +63 -0
- package/backend/src/flowent/routes/workspace.py +105 -0
- package/backend/src/flowent/sandbox.py +1 -1
- package/backend/src/flowent/state/__init__.py +53 -0
- package/backend/src/flowent/state/models.py +257 -0
- package/backend/src/flowent/state/schema.py +186 -0
- package/backend/src/flowent/state/store.py +1013 -0
- package/backend/src/flowent/static/assets/index-CvWZZMtK.css +2 -0
- package/backend/src/flowent/static/assets/index-ma2v8oW7.js +90 -0
- package/backend/src/flowent/static/index.html +2 -2
- package/backend/src/flowent/storage.py +52 -1254
- package/backend/src/flowent/system_tools.py +25 -0
- package/backend/src/flowent/tools.py +4 -2
- package/backend/src/flowent/usage.py +9 -4
- package/backend/src/flowent/workflows.py +282 -0
- package/backend/src/flowent/workspace/__init__.py +0 -0
- package/backend/src/flowent/workspace/context.py +249 -0
- package/backend/src/flowent/workspace/events.py +180 -0
- package/backend/src/flowent/workspace/output.py +274 -0
- package/backend/src/flowent/workspace/runtime.py +1041 -0
- package/backend/uv.lock +1 -1
- package/dist/frontend/assets/index-CvWZZMtK.css +2 -0
- package/dist/frontend/assets/index-ma2v8oW7.js +90 -0
- package/dist/frontend/index.html +2 -2
- package/package.json +1 -1
- package/backend/src/flowent/static/assets/index-D7t9qNrC.js +0 -82
- package/backend/src/flowent/static/assets/index-DufpDl8x.css +0 -2
- package/dist/frontend/assets/index-D7t9qNrC.js +0 -82
- package/dist/frontend/assets/index-DufpDl8x.css +0 -2
package/README.md
CHANGED
|
@@ -17,11 +17,11 @@ A workflow orchestration platform for multi-agent collaboration.
|
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
20
|
-
Flowent requires Bubblewrap for local tool isolation
|
|
21
|
-
first:
|
|
20
|
+
Flowent requires Bubblewrap for local tool isolation and ripgrep for file
|
|
21
|
+
search. Install the system packages first:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
sudo apt-get install bubblewrap
|
|
24
|
+
sudo apt-get install bubblewrap ripgrep
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
Install the CLI globally:
|
package/backend/README.md
CHANGED
|
@@ -17,11 +17,11 @@ A workflow orchestration platform for multi-agent collaboration.
|
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
20
|
-
Flowent requires Bubblewrap for local tool isolation
|
|
21
|
-
first:
|
|
20
|
+
Flowent requires Bubblewrap for local tool isolation and ripgrep for file
|
|
21
|
+
search. Install the system packages first:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
sudo apt-get install bubblewrap
|
|
24
|
+
sudo apt-get install bubblewrap ripgrep
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
Install the CLI globally:
|
package/backend/pyproject.toml
CHANGED
|
@@ -45,7 +45,7 @@ Use tools deliberately:
|
|
|
45
45
|
- Search the web only when current external information is needed.
|
|
46
46
|
- Update the plan when a task has multiple meaningful steps.
|
|
47
47
|
|
|
48
|
-
After each tool result, decide whether the task is complete, whether another tool is needed, or whether you need to explain a blocker. When no more tool work is needed, provide the final response."""
|
|
48
|
+
After each tool result, decide whether the task is complete, whether another tool is needed, or whether you need to explain a blocker. A tool call is not a final response. After every tool result, continue the same turn until you either call another tool, explain a blocker, or provide a final response. If a tool fails, use the error as context and continue deciding whether to retry, use another tool, or explain the blocker. When no more tool work is needed, provide the final response."""
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
class AgentStreamEvent(BaseModel):
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
from flowent.llm import ProviderFormat
|
|
6
|
+
from flowent.storage import StoredMessage, StoredWritablePath
|
|
7
|
+
from flowent.usage import TokenUsageInfo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProviderModelsRequest(BaseModel):
|
|
11
|
+
model_config = ConfigDict(extra="forbid")
|
|
12
|
+
|
|
13
|
+
provider: ProviderFormat
|
|
14
|
+
secret_reference: str
|
|
15
|
+
base_url: str | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProviderModelsResponse(BaseModel):
|
|
19
|
+
models: list[str]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WorkspaceMessagesRequest(BaseModel):
|
|
23
|
+
model_config = ConfigDict(extra="forbid")
|
|
24
|
+
|
|
25
|
+
messages: list[StoredMessage]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WorkspaceMessageEditRequest(BaseModel):
|
|
29
|
+
model_config = ConfigDict(extra="forbid")
|
|
30
|
+
|
|
31
|
+
action: Literal["resend", "save"]
|
|
32
|
+
content: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class WorkspaceMessageEditResponse(BaseModel):
|
|
36
|
+
model_config = ConfigDict(extra="forbid")
|
|
37
|
+
|
|
38
|
+
messages: list[StoredMessage]
|
|
39
|
+
run_id: str | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class WorkspaceRespondRequest(BaseModel):
|
|
43
|
+
model_config = ConfigDict(extra="forbid")
|
|
44
|
+
|
|
45
|
+
content: str
|
|
46
|
+
message_id: str | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class WorkspaceRunResponse(BaseModel):
|
|
50
|
+
model_config = ConfigDict(extra="forbid")
|
|
51
|
+
|
|
52
|
+
run_id: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class WorkspaceClearResponse(BaseModel):
|
|
56
|
+
model_config = ConfigDict(extra="forbid")
|
|
57
|
+
|
|
58
|
+
active_run_id: str | None = None
|
|
59
|
+
messages: list[StoredMessage]
|
|
60
|
+
usage_info: TokenUsageInfo | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AboutResponse(BaseModel):
|
|
64
|
+
model_config = ConfigDict(extra="forbid")
|
|
65
|
+
|
|
66
|
+
version: str
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TelegramSessionApproveRequest(BaseModel):
|
|
70
|
+
model_config = ConfigDict(extra="forbid")
|
|
71
|
+
|
|
72
|
+
chat_id: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SkillSettingsRequest(BaseModel):
|
|
76
|
+
model_config = ConfigDict(extra="forbid")
|
|
77
|
+
|
|
78
|
+
enabled: bool
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class McpImportRequest(BaseModel):
|
|
82
|
+
model_config = ConfigDict(extra="forbid")
|
|
83
|
+
|
|
84
|
+
server_id: str
|
|
85
|
+
source: Literal["claude_code", "codex"]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class McpImportPreviewRequest(BaseModel):
|
|
89
|
+
model_config = ConfigDict(extra="forbid")
|
|
90
|
+
|
|
91
|
+
source: Literal["claude_code", "codex"]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class WritablePathRequest(BaseModel):
|
|
95
|
+
model_config = ConfigDict(extra="forbid")
|
|
96
|
+
|
|
97
|
+
path: str
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class WritablePathListResponse(BaseModel):
|
|
101
|
+
model_config = ConfigDict(extra="forbid")
|
|
102
|
+
|
|
103
|
+
writable_paths: list[StoredWritablePath]
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from collections.abc import AsyncIterator, Awaitable
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from fastapi.responses import FileResponse
|
|
9
|
+
from fastapi.staticfiles import StaticFiles
|
|
10
|
+
|
|
11
|
+
from flowent.channels import TelegramBotManager, TelegramTransport
|
|
12
|
+
from flowent.compact import LocalSummaryCompactProvider
|
|
13
|
+
from flowent.llm import CompletionCallable
|
|
14
|
+
from flowent.logging import ensure_logging_configured
|
|
15
|
+
from flowent.mcp import McpManager, McpTransport
|
|
16
|
+
from flowent.paths import resolve_workdir
|
|
17
|
+
from flowent.routes.integrations import register_integration_routes
|
|
18
|
+
from flowent.routes.permissions import register_permission_routes
|
|
19
|
+
from flowent.routes.providers import register_provider_routes
|
|
20
|
+
from flowent.routes.system import register_system_routes
|
|
21
|
+
from flowent.routes.workflow_routes import register_workflow_routes
|
|
22
|
+
from flowent.routes.workspace import register_workspace_routes
|
|
23
|
+
from flowent.sandbox import ensure_sandbox_available
|
|
24
|
+
from flowent.storage import StateStore
|
|
25
|
+
from flowent.system_tools import ensure_ripgrep_available
|
|
26
|
+
from flowent.workspace.runtime import WorkspaceRuntime
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("flowent.app")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
DEFAULT_STATIC_DIR = Path(__file__).parent / "static"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def frontend_static_directory() -> Path:
|
|
35
|
+
configured_directory = os.environ.get("FLOWENT_STATIC_DIR")
|
|
36
|
+
if configured_directory:
|
|
37
|
+
return Path(configured_directory)
|
|
38
|
+
repository_frontend_dist = Path(__file__).resolve().parents[3] / "frontend" / "dist"
|
|
39
|
+
if repository_frontend_dist.is_dir():
|
|
40
|
+
return repository_frontend_dist
|
|
41
|
+
return DEFAULT_STATIC_DIR
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def create_app(
|
|
45
|
+
*,
|
|
46
|
+
serve_frontend: bool = True,
|
|
47
|
+
chat_completion: CompletionCallable | None = None,
|
|
48
|
+
mcp_transport: McpTransport | None = None,
|
|
49
|
+
telegram_transport: TelegramTransport | None = None,
|
|
50
|
+
workdir: Path | str | None = None,
|
|
51
|
+
) -> FastAPI:
|
|
52
|
+
ensure_logging_configured()
|
|
53
|
+
ensure_sandbox_available()
|
|
54
|
+
ensure_ripgrep_available()
|
|
55
|
+
|
|
56
|
+
cwd = resolve_workdir(workdir)
|
|
57
|
+
store = StateStore()
|
|
58
|
+
compact_provider = LocalSummaryCompactProvider()
|
|
59
|
+
mcp_manager = McpManager(store=store, transport=mcp_transport)
|
|
60
|
+
|
|
61
|
+
static_dir = frontend_static_directory().resolve(strict=False)
|
|
62
|
+
logger.debug("Flowent app created serve_frontend=%s", serve_frontend)
|
|
63
|
+
logger.info("Workdir: %s", cwd)
|
|
64
|
+
logger.info("Static directory: %s", static_dir)
|
|
65
|
+
|
|
66
|
+
runtime = WorkspaceRuntime(
|
|
67
|
+
chat_completion=chat_completion,
|
|
68
|
+
compact_provider=compact_provider,
|
|
69
|
+
cwd=cwd,
|
|
70
|
+
mcp_manager=mcp_manager,
|
|
71
|
+
store=store,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
telegram_bot_manager = TelegramBotManager(
|
|
75
|
+
message_handler=runtime.reply_text,
|
|
76
|
+
store=store,
|
|
77
|
+
telegram_transport=telegram_transport,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
async def run_shutdown_step(label: str, cleanup: Awaitable[object]) -> None:
|
|
81
|
+
try:
|
|
82
|
+
await cleanup
|
|
83
|
+
except Exception:
|
|
84
|
+
logger.exception("%s cleanup failed during shutdown", label)
|
|
85
|
+
|
|
86
|
+
async def graceful_shutdown() -> None:
|
|
87
|
+
await run_shutdown_step("Workspace", runtime.stop_for_shutdown())
|
|
88
|
+
await run_shutdown_step("Telegram", telegram_bot_manager.stop_all())
|
|
89
|
+
await run_shutdown_step("MCP", mcp_manager.stop_all())
|
|
90
|
+
|
|
91
|
+
@asynccontextmanager
|
|
92
|
+
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
93
|
+
app.state.mcp_manager = mcp_manager
|
|
94
|
+
app.state.telegram_bot_manager = telegram_bot_manager
|
|
95
|
+
await mcp_manager.start_enabled()
|
|
96
|
+
await telegram_bot_manager.start_enabled()
|
|
97
|
+
try:
|
|
98
|
+
yield
|
|
99
|
+
finally:
|
|
100
|
+
await graceful_shutdown()
|
|
101
|
+
|
|
102
|
+
app = FastAPI(title="Flowent", lifespan=lifespan)
|
|
103
|
+
app.state.mcp_manager = mcp_manager
|
|
104
|
+
app.state.telegram_bot_manager = telegram_bot_manager
|
|
105
|
+
|
|
106
|
+
register_system_routes(
|
|
107
|
+
app,
|
|
108
|
+
cwd=cwd,
|
|
109
|
+
mcp_manager=mcp_manager,
|
|
110
|
+
runtime=runtime,
|
|
111
|
+
store=store,
|
|
112
|
+
telegram_bot_manager=telegram_bot_manager,
|
|
113
|
+
)
|
|
114
|
+
register_provider_routes(app, store=store)
|
|
115
|
+
register_integration_routes(
|
|
116
|
+
app,
|
|
117
|
+
cwd=cwd,
|
|
118
|
+
mcp_manager=mcp_manager,
|
|
119
|
+
store=store,
|
|
120
|
+
telegram_bot_manager=telegram_bot_manager,
|
|
121
|
+
)
|
|
122
|
+
register_workflow_routes(
|
|
123
|
+
app,
|
|
124
|
+
chat_completion=chat_completion,
|
|
125
|
+
store=store,
|
|
126
|
+
)
|
|
127
|
+
register_permission_routes(app, cwd=cwd, store=store)
|
|
128
|
+
register_workspace_routes(app, runtime=runtime, store=store)
|
|
129
|
+
|
|
130
|
+
if serve_frontend and static_dir.is_dir():
|
|
131
|
+
assets_dir = static_dir / "assets"
|
|
132
|
+
if assets_dir.is_dir():
|
|
133
|
+
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
|
|
134
|
+
|
|
135
|
+
@app.get("/{path:path}")
|
|
136
|
+
async def spa_fallback(path: str) -> FileResponse:
|
|
137
|
+
file = (static_dir / path).resolve(strict=False)
|
|
138
|
+
if file.is_file() and file.is_relative_to(static_dir):
|
|
139
|
+
return FileResponse(file)
|
|
140
|
+
return FileResponse(static_dir / "index.html")
|
|
141
|
+
|
|
142
|
+
return app
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
app = create_app()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
import uvicorn
|
|
150
|
+
|
|
151
|
+
uvicorn.run(app)
|
|
@@ -59,13 +59,22 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
59
59
|
|
|
60
60
|
if args.command == "doctor":
|
|
61
61
|
from flowent.sandbox import SANDBOX_INSTALL_HINT, sandbox_binary
|
|
62
|
+
from flowent.system_tools import RIPGREP_INSTALL_HINT, ripgrep_binary
|
|
62
63
|
|
|
63
64
|
bwrap = sandbox_binary()
|
|
65
|
+
rg = ripgrep_binary()
|
|
66
|
+
|
|
64
67
|
if bwrap:
|
|
65
68
|
print(f"Sandbox: {bwrap}")
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
else:
|
|
70
|
+
print(f"Sandbox: missing. {SANDBOX_INSTALL_HINT}", file=sys.stderr)
|
|
71
|
+
|
|
72
|
+
if rg:
|
|
73
|
+
print(f"Search: {rg}")
|
|
74
|
+
else:
|
|
75
|
+
print(f"Search: missing. {RIPGREP_INSTALL_HINT}", file=sys.stderr)
|
|
76
|
+
|
|
77
|
+
raise SystemExit(0 if bwrap and rg else 1)
|
|
69
78
|
|
|
70
79
|
if args.version:
|
|
71
80
|
try:
|
|
@@ -95,7 +104,7 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
95
104
|
import uvicorn
|
|
96
105
|
|
|
97
106
|
uvicorn.run(
|
|
98
|
-
"flowent.
|
|
107
|
+
"flowent.app:app",
|
|
99
108
|
host=args.host,
|
|
100
109
|
port=args.port,
|
|
101
110
|
)
|
|
@@ -10,7 +10,7 @@ from flowent.llm import (
|
|
|
10
10
|
ProviderConnection,
|
|
11
11
|
complete_chat_with_usage,
|
|
12
12
|
)
|
|
13
|
-
from flowent.usage import TokenUsage
|
|
13
|
+
from flowent.usage import APPROX_BYTES_PER_TOKEN, TokenUsage, approximate_token_count
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
from flowent.storage import StoredMessage
|
|
@@ -172,14 +172,41 @@ def retained_recent_user_messages(
|
|
|
172
172
|
def truncate_text_to_token_budget(content: str, token_budget: int) -> str:
|
|
173
173
|
if token_budget <= 0 or not content:
|
|
174
174
|
return ""
|
|
175
|
-
|
|
176
|
-
if len(content) <=
|
|
175
|
+
byte_budget = max(token_budget * APPROX_BYTES_PER_TOKEN, 1)
|
|
176
|
+
if len(content.encode("utf-8")) <= byte_budget:
|
|
177
177
|
return content
|
|
178
|
-
left_budget =
|
|
179
|
-
right_budget =
|
|
180
|
-
|
|
178
|
+
left_budget = byte_budget // 2
|
|
179
|
+
right_budget = byte_budget - left_budget
|
|
180
|
+
prefix = text_prefix_for_byte_budget(content, left_budget)
|
|
181
|
+
suffix = text_suffix_for_byte_budget(content, right_budget)
|
|
182
|
+
middle_end = -len(suffix) if suffix else len(content)
|
|
183
|
+
removed_tokens = approximate_token_count(content[len(prefix) : middle_end])
|
|
181
184
|
marker = f"…{removed_tokens} tokens truncated…"
|
|
182
|
-
return f"{
|
|
185
|
+
return f"{prefix}{marker}{suffix}"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def text_prefix_for_byte_budget(content: str, byte_budget: int) -> str:
|
|
189
|
+
used_bytes = 0
|
|
190
|
+
prefix: list[str] = []
|
|
191
|
+
for character in content:
|
|
192
|
+
character_bytes = len(character.encode("utf-8"))
|
|
193
|
+
if used_bytes + character_bytes > byte_budget:
|
|
194
|
+
break
|
|
195
|
+
prefix.append(character)
|
|
196
|
+
used_bytes += character_bytes
|
|
197
|
+
return "".join(prefix)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def text_suffix_for_byte_budget(content: str, byte_budget: int) -> str:
|
|
201
|
+
used_bytes = 0
|
|
202
|
+
suffix: list[str] = []
|
|
203
|
+
for character in reversed(content):
|
|
204
|
+
character_bytes = len(character.encode("utf-8"))
|
|
205
|
+
if used_bytes + character_bytes > byte_budget:
|
|
206
|
+
break
|
|
207
|
+
suffix.append(character)
|
|
208
|
+
used_bytes += character_bytes
|
|
209
|
+
return "".join(reversed(suffix))
|
|
183
210
|
|
|
184
211
|
|
|
185
212
|
def transcript_messages_after(
|
|
@@ -196,9 +223,3 @@ def transcript_messages_after(
|
|
|
196
223
|
|
|
197
224
|
def approximate_tokens_for_messages(messages: Sequence[ChatMessage]) -> int:
|
|
198
225
|
return sum(approximate_token_count(message.content) for message in messages)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def approximate_token_count(content: str) -> int:
|
|
202
|
-
if not content:
|
|
203
|
-
return 0
|
|
204
|
-
return max(1, (len(content) + 3) // 4)
|
|
@@ -13,6 +13,7 @@ from flowent.logging import (
|
|
|
13
13
|
configure_litellm_logging,
|
|
14
14
|
write_llm_request_diagnostic,
|
|
15
15
|
)
|
|
16
|
+
from flowent.network import flowent_user_agent
|
|
16
17
|
from flowent.usage import TokenUsage, token_usage_from_response
|
|
17
18
|
|
|
18
19
|
|
|
@@ -175,17 +176,13 @@ def normalize_provider_model_name(provider: ProviderFormat, model: str) -> str:
|
|
|
175
176
|
|
|
176
177
|
|
|
177
178
|
def stream_failure_message(chunk: Any) -> str:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if not isinstance(chunk, Mapping):
|
|
181
|
-
return ""
|
|
182
|
-
|
|
183
|
-
event_type = getattr(chunk.get("type"), "value", chunk.get("type"))
|
|
179
|
+
event_type = value_at(chunk, "type", "")
|
|
180
|
+
event_type = getattr(event_type, "value", event_type)
|
|
184
181
|
event_type = str(event_type or "")
|
|
185
182
|
if event_type == "error":
|
|
186
|
-
error = chunk
|
|
183
|
+
error = value_at(chunk, "error", {})
|
|
187
184
|
elif event_type == "response.failed":
|
|
188
|
-
response = chunk
|
|
185
|
+
response = value_at(chunk, "response", {})
|
|
189
186
|
error = value_at(response, "error", {})
|
|
190
187
|
else:
|
|
191
188
|
return ""
|
|
@@ -304,6 +301,7 @@ def build_litellm_request(
|
|
|
304
301
|
)
|
|
305
302
|
request: dict[str, Any] = {
|
|
306
303
|
"api_key": connection.secret_reference,
|
|
304
|
+
"extra_headers": {"User-Agent": flowent_user_agent()},
|
|
307
305
|
"messages": request_messages,
|
|
308
306
|
"model": provider_model_name(connection),
|
|
309
307
|
}
|
|
@@ -194,6 +194,12 @@ class ConsoleNoiseFilter(logging.Filter):
|
|
|
194
194
|
return record.levelno > logging.DEBUG or record.name.startswith("flowent")
|
|
195
195
|
|
|
196
196
|
|
|
197
|
+
class ConsoleHandler(logging.StreamHandler):
|
|
198
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
199
|
+
self.setStream(sys.stderr if record.levelno >= logging.WARNING else sys.stdout)
|
|
200
|
+
super().emit(record)
|
|
201
|
+
|
|
202
|
+
|
|
197
203
|
def configure_logging(*, directory: Path | None = None) -> Path:
|
|
198
204
|
global _configured_log_file, _configured_log_process_id
|
|
199
205
|
|
|
@@ -218,7 +224,7 @@ def configure_logging(*, directory: Path | None = None) -> Path:
|
|
|
218
224
|
)
|
|
219
225
|
)
|
|
220
226
|
|
|
221
|
-
console_handler =
|
|
227
|
+
console_handler = ConsoleHandler()
|
|
222
228
|
console_handler.setLevel(console_log_level())
|
|
223
229
|
console_handler.setFormatter(
|
|
224
230
|
RedactingFormatter("%(levelname)s %(name)s: %(message)s")
|