flowtraicer 0.9.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.
@@ -0,0 +1,467 @@
1
+ Metadata-Version: 2.4
2
+ Name: flowtraicer
3
+ Version: 0.9.0
4
+ Summary: Map, visualize, monitor, debug, log and audit the steps of an engagement between a user and an agentic AI system.
5
+ Project-URL: Homepage, https://github.com/AlirezaShk/flowtraicer
6
+ Project-URL: Repository, https://github.com/AlirezaShk/flowtraicer
7
+ Project-URL: Issues, https://github.com/AlirezaShk/flowtraicer/issues
8
+ Project-URL: Changelog, https://github.com/AlirezaShk/flowtraicer/blob/main/CHANGELOG.md
9
+ Author: TeranoAI
10
+ License: Apache-2.0
11
+ License-File: LICENSE
12
+ Keywords: agents,audit,langgraph,llm,observability,tracing
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Topic :: System :: Monitoring
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: fastapi>=0.110
25
+ Requires-Dist: langchain-core>=0.3
26
+ Requires-Dist: langgraph>=0.2
27
+ Requires-Dist: pydantic>=2.7
28
+ Requires-Dist: uvicorn>=0.29
29
+ Provides-Extra: anthropic
30
+ Requires-Dist: anthropic>=0.40; extra == 'anthropic'
31
+ Provides-Extra: dev
32
+ Requires-Dist: fakeredis>=2.20; extra == 'dev'
33
+ Requires-Dist: httpx>=0.27; extra == 'dev'
34
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
35
+ Requires-Dist: pytest>=8.0; extra == 'dev'
36
+ Requires-Dist: ruff>=0.4; extra == 'dev'
37
+ Provides-Extra: extraction
38
+ Requires-Dist: instructor>=1.0; extra == 'extraction'
39
+ Provides-Extra: google
40
+ Requires-Dist: google-genai>=1.0; extra == 'google'
41
+ Provides-Extra: litellm
42
+ Requires-Dist: litellm>=1.40; extra == 'litellm'
43
+ Provides-Extra: openai
44
+ Requires-Dist: openai>=1.0; extra == 'openai'
45
+ Provides-Extra: postgres
46
+ Requires-Dist: psycopg[binary]>=3.1; extra == 'postgres'
47
+ Provides-Extra: providers
48
+ Requires-Dist: anthropic>=0.40; extra == 'providers'
49
+ Requires-Dist: google-genai>=1.0; extra == 'providers'
50
+ Requires-Dist: openai>=1.0; extra == 'providers'
51
+ Provides-Extra: redis
52
+ Requires-Dist: redis>=5; extra == 'redis'
53
+ Description-Content-Type: text/markdown
54
+
55
+ # FlowTraicer
56
+
57
+ ## From the Human Author
58
+
59
+ I couldn't find an intuitive package that JUST works when it comes to visualizing, monitoring, auditing, debugging, and orchestrating agentic workflows in a systematic way. So I just decided to build one with Claude. Feel free to use this, and contribute to it if you feel like it!
60
+
61
+ ## Short Introduction
62
+
63
+ An open source Python library to **map, visualize, monitor, debug, log, and audit** the steps of an engagement between a user and an agentic AI system.
64
+
65
+ You build your agent as a [LangGraph](https://github.com/langchain-ai/langgraph) graph;
66
+ `FlowTraicer` captures each run as a structured, append-only trace and renders it as a linked
67
+ **graph + timeline** in a browser.
68
+
69
+ ## The model
70
+
71
+ An engagement is a three-level tree:
72
+
73
+ ```text
74
+ Engagement → Step (one workflow node) → Events (llm_call / tool_call / extraction / log / error)
75
+ ```
76
+
77
+ - **Engagement** — one whole user↔agent session. Carries free-form `metadata` (put your
78
+ `user_id`, `session_id`, etc. here).
79
+ - **Step** — one LangGraph node execution. Has a status, timing, the **tools available** to
80
+ it, and an optional **per-step extraction** (a Pydantic schema pulled from the turn).
81
+ - **Event** — something that happened inside a step (a tool call, an LLM call, a log line,
82
+ an error, an extraction).
83
+ - **Global step** — a node that can fire from anywhere and re-route the workflow's intent
84
+ (e.g. "escalate to a human"). Entering one records an **intent switch**.
85
+
86
+ ## Install
87
+
88
+ ```bash
89
+ pip install -e ".[dev]" # core + test deps
90
+ pip install -e ".[extraction,openai]" # + Instructor extraction with the OpenAI SDK
91
+ ```
92
+
93
+ (Requires Python ≥ 3.11. The package imports as `FlowTraicer` regardless of the `src/` layout.)
94
+
95
+ ## Getting started: instrument a LangGraph agent
96
+
97
+ ```python
98
+ import asyncio
99
+ from typing import Annotated, TypedDict
100
+ from operator import add
101
+
102
+ from langgraph.graph import StateGraph, START, END
103
+
104
+ from ft.store.sqlite import SQLiteStore
105
+ from ft.recorder import Recorder
106
+ from ft.langgraph_adapter import run_instrumented
107
+
108
+
109
+ # 1. Build your agent as a normal LangGraph graph. Extend TraceState so you don't
110
+ # redeclare FlowTraicer's channels (tool_calls / llm_calls / events / extraction) yourself.
111
+ from ft.langgraph_adapter import TraceState
112
+
113
+ class State(TraceState):
114
+ messages: Annotated[list, add] # your own domain fields
115
+
116
+ def greet(state): return {"messages": ["hi, what are you looking for?"]}
117
+ def search(state): return {"messages": ["here are 3 options"],
118
+ "tool_calls": [{"name": "search_db", "payload": {"hits": 3}}]}
119
+
120
+ g = StateGraph(State)
121
+ g.add_node("greet", greet)
122
+ g.add_node("search", search)
123
+ g.add_edge(START, "greet")
124
+ g.add_edge("greet", "search")
125
+ g.add_edge("search", END)
126
+ app = g.compile()
127
+
128
+
129
+ # 2. Pick a store. SQLite is the zero-dependency default; pass a path to persist.
130
+ store = SQLiteStore("traces.db")
131
+ recorder = Recorder(store)
132
+
133
+ # 3. Run it under instrumentation. Everything the run does is recorded.
134
+ engagement_id = asyncio.run(run_instrumented(
135
+ app,
136
+ {"messages": [], "tool_calls": []},
137
+ recorder,
138
+ name="house_search",
139
+ metadata={"user_id": "u-42", "session_id": "s-1"}, # tag the journey
140
+ global_nodes={"escalate"}, # nodes that re-route intent
141
+ node_tools={"search": ["search_db"]}, # tools available per step
142
+ ))
143
+
144
+ # 4. Read the trace back.
145
+ engagement = store.get_engagement(engagement_id)
146
+ for step in engagement.steps:
147
+ print(step.name, step.status, f"{step.duration_ms:.1f}ms",
148
+ [e.name for e in step.events])
149
+ ```
150
+
151
+ ### Enriching a step from inside a node
152
+
153
+ `run_instrumented` records node entry/exit, timing, and intent switches automatically. To
154
+ capture more, a node may write these **conventional state keys** — the runner records
155
+ whatever it finds, so nodes never need a handle to the recorder:
156
+
157
+ ```python
158
+ def qualify(state):
159
+ return {
160
+ "messages": ["got it"],
161
+ # tools the node called:
162
+ "tool_calls": [{"name": "lookup_area", "payload": {"area": "Shibuya"}}],
163
+ # LLM calls with token cost (rolls up into step.total_tokens / engagement.total_tokens):
164
+ "llm_calls": [{"name": "gpt-4o-mini", "prompt_tokens": 64, "completion_tokens": 20}],
165
+ # any other typed event (kind = llm_call|tool_call|extraction|log|error):
166
+ "events": [{"kind": "log", "name": "cache_hit", "payload": {"key": "shibuya"}}],
167
+ # a per-step structured extraction:
168
+ "extraction": {"schema_name": "BudgetInfo", "values": {"budget": 95000}},
169
+ }
170
+ ```
171
+
172
+ `State` must declare these keys (use `Annotated[list, add]` for the list-valued ones).
173
+ Outside a graph you can also call the recorder directly:
174
+ `recorder.record_llm_call(step_id, "gpt-4o", prompt=64, completion=20)`.
175
+
176
+ ### Drop-off: goals & abandonment
177
+
178
+ Pass `goal_nodes` to mark journeys that never reached a goal as **abandoned** (instead of
179
+ the indistinguishable `completed`), with `dropped_at` set to the last step reached:
180
+
181
+ ```python
182
+ await run_instrumented(app, state, recorder,
183
+ goal_nodes={"submitted"}) # didn't reach it -> ABANDONED
184
+ ```
185
+
186
+ ### Free-form LLM calls — bring your own provider
187
+
188
+ FlowTraicer's core only *records* tokens; **it never imports a specific LLM SDK.** The single
189
+ integration point is the `LLMClient` protocol (`ft.llm.LLMClient`): one async method returning the
190
+ completion text and its token usage.
191
+
192
+ ```python
193
+ from ft.llm import LLMClient # a runtime_checkable Protocol
194
+
195
+ class LLMClient(Protocol):
196
+ async def acomplete(self, messages, **overrides) -> LLMResult: ... # text + token usage
197
+ ```
198
+
199
+ When a node calls `await ctx.llm(prompt)`, the workflow calls `acomplete` on whatever you passed as
200
+ `llm=` to `Workflow.run(...)`, then records the returned tokens. So you can plug in **any** provider
201
+ or SDK — there's no LiteLLM requirement.
202
+
203
+ **The bundled option** is `LiteLLMClient`, which wraps [LiteLLM](https://docs.litellm.ai) so one
204
+ config targets 100+ providers (install the `litellm` extra):
205
+
206
+ ```python
207
+ from ft.llm import LiteLLMClient
208
+
209
+ llm = LiteLLMClient(provider="openai", model="gpt-5-nano", api_key="XXX")
210
+ # or from a config blob: LiteLLMClient.from_config({"llm_provider": "openai", "model": "...", "key": "..."})
211
+ engagement_id = await workflow.run(state, recorder, llm=llm)
212
+ ```
213
+
214
+ #### Adding your own provider / SDK
215
+
216
+ Implement `acomplete` and return an `LLMResult` (reuse it — `TokenUsage` derives `total` from
217
+ `prompt + completion` when you don't set it, and `as_llm_call()` is handled for you):
218
+
219
+ ```python
220
+ from ft.core.model import TokenUsage
221
+ from ft.llm import LLMResult # any object with `.text` + `.as_llm_call()` works; LLMResult is the easy path
222
+
223
+ class MyGeminiClient:
224
+ """Adapt your existing SDK/client to FlowTraicer. `acomplete` is the only method ctx.llm needs."""
225
+ def __init__(self, sdk):
226
+ self._sdk = sdk
227
+
228
+ async def acomplete(self, messages, **overrides) -> LLMResult:
229
+ resp = await self._sdk.generate(messages, **overrides) # however your SDK returns text + usage
230
+ return LLMResult(
231
+ text=resp.text,
232
+ tokens=TokenUsage(prompt=resp.usage.input, completion=resp.usage.output),
233
+ model=resp.model,
234
+ )
235
+
236
+ # isinstance(MyGeminiClient(sdk), LLMClient) is True (structural check), and:
237
+ await workflow.run(state, recorder, llm=MyGeminiClient(sdk)) # tokens flow into every step's trace
238
+ ```
239
+
240
+ Notes:
241
+ - **Sync SDK?** Wrap the blocking call so you don't stall the event loop:
242
+ `return await anyio.to_thread.run_sync(lambda: self._complete(messages))`.
243
+ - **Capture usage.** The whole point of `ctx.llm` is token accounting — read your SDK's usage
244
+ field (e.g. Gemini's `response.usage_metadata`, OpenAI's `response.usage`) into `TokenUsage`.
245
+ - **Per-call overrides.** `ctx.llm(prompt, model="…", temperature=0)` forwards `**overrides` to
246
+ `acomplete`; honor what you support and ignore the rest.
247
+
248
+ #### Setting a global default provider
249
+
250
+ Passing `llm=` to every `run()` gets repetitive. Register one **global default** instead — every
251
+ workflow falls back to it when no per-run / per-workflow client is given:
252
+
253
+ ```python
254
+ from ft.llm import LiteLLMClient
255
+ from ft.registry import REGISTER
256
+
257
+ # LiteLLM is the bundled default provider; register a configured instance once at startup:
258
+ REGISTER.set_llm_provider(LiteLLMClient(provider="openai", model="gpt-5-nano", api_key=KEY))
259
+ # ...or your own client — set_llm_provider VALIDATES it satisfies LLMClient first:
260
+ REGISTER.set_llm_provider(MyGeminiClient(sdk)) # TypeError if it has no async acomplete()
261
+ ```
262
+
263
+ Resolution order when a node calls `ctx.llm`:
264
+
265
+ ```text
266
+ run(llm=…) > Workflow(llm=…) > REGISTER.get_llm_provider()
267
+ ```
268
+
269
+ So the global is the lowest-priority fallback: a per-run `llm=` (e.g. a request-scoped client)
270
+ always wins. `REGISTER.set_llm_provider` asserts the client exposes a callable, async
271
+ `acomplete(messages, **overrides)` and raises a descriptive `TypeError` otherwise — you can't
272
+ register a provider that won't work at run time.
273
+
274
+ #### Token usage for LLM calls made *outside* a workflow
275
+
276
+ Not every LLM call runs inside a `Workflow` — chat replies, voice turns, and one-off extractions
277
+ often call a model directly. Register a global recorder and push their token usage into FlowTraicer
278
+ so it rolls up alongside everything else:
279
+
280
+ ```python
281
+ from ft.core.model import TokenUsage
282
+ from ft.recorder import Recorder
283
+ from ft.registry import REGISTER
284
+ from ft.store.postgres import PostgresStore
285
+
286
+ REGISTER.set_recorder(Recorder(PostgresStore(DSN))) # validated on registration
287
+
288
+ # wherever you make a model call (e.g. inside an SDK adapter), after you have the usage:
289
+ REGISTER.record_llm_usage(
290
+ "openai/gpt-5-nano",
291
+ tokens=TokenUsage(prompt=resp.usage.prompt_tokens, completion=resp.usage.completion_tokens),
292
+ caller="instagram_dm.classifier", # shows up in the viewer / group_by
293
+ metadata={"session_id": session_id},
294
+ )
295
+ ```
296
+
297
+ Each call becomes a small self-contained engagement (one `llm_call` event), so per-model /
298
+ per-caller token totals appear in the viewer and `analytics.group_by`. It's **fail-open**: with no
299
+ recorder registered it's a no-op, and a recorder error never propagates into the calling agent.
300
+
301
+ (Structured extraction uses Instructor; see below.)
302
+
303
+ ### Per-step schema extraction (Instructor + Pydantic)
304
+
305
+ ```python
306
+ from pydantic import BaseModel
307
+ from ft.extraction import Extractor
308
+
309
+ class BudgetInfo(BaseModel):
310
+ budget: int
311
+ area: str
312
+
313
+ extractor = Extractor.from_provider("openai/gpt-4o-mini") # or anthropic/… , google/…
314
+ result = extractor.extract(BudgetInfo, "Shibuya, around ¥95,000")
315
+
316
+ # inside a node — record-via-state (the runner picks it up):
317
+ return {"extraction": result.as_record().model_dump()}
318
+ # anywhere else — record directly:
319
+ extractor.extract_and_record(recorder, step_id, BudgetInfo, "Shibuya, around ¥95,000")
320
+ ```
321
+
322
+ ## Declarative workflows (the DSL)
323
+
324
+ `ft.orchestration.Workflow` is sugar over LangGraph: declare steps (with tools), global
325
+ steps, goals, and edges once — it compiles the graph and wires per-step tools / global nodes
326
+ / goal nodes into the recorder, so there's no separate bookkeeping to pass to the runner.
327
+
328
+ ```python
329
+ from ft.orchestration import Workflow
330
+
331
+ wf = Workflow("school_journey", state_schema=State, goal_nodes={"submit"})
332
+
333
+ @wf.step(tools=["search_schools"])
334
+ def school_selection(state): ...
335
+
336
+ @wf.global_step # entering it records an intent switch
337
+ def escalate(state): ...
338
+
339
+ wf.entry("intake")
340
+ wf.edge("intake", "school_selection")
341
+ wf.branch("school_selection", router, {"compare": "comparison", "apply": "consent"})
342
+ wf.finish("submit")
343
+
344
+ engagement_id = await wf.run(initial_state, recorder, metadata={"user_id": "u1"})
345
+ ```
346
+
347
+ (The hand-wired `run_instrumented` approach still works; the DSL is optional sugar.)
348
+
349
+ ## Storage backends
350
+
351
+ The `Store` is pluggable (append-only: write a record, reconstruct an engagement, list
352
+ summaries, subscribe to a live tail). Pick by environment — the trace, viewer, and analytics
353
+ work identically on all three:
354
+
355
+ ```python
356
+ from ft.store.sqlite import SQLiteStore # default; zero deps, file or :memory:
357
+ store = SQLiteStore("traces.db")
358
+
359
+ from ft.store.redis import RedisStore # pip install -e ".[redis]"
360
+ store = RedisStore("redis://localhost:6379") # Redis Streams; live tail across processes
361
+
362
+ from ft.store.postgres import PostgresStore # pip install -e ".[postgres]"
363
+ store = PostgresStore("postgresql://localhost/FlowTraicer") # durable JSONB + LISTEN/NOTIFY
364
+ ```
365
+
366
+ - **SQLite** — local dev, single process, audit-friendly append-only file.
367
+ - **Redis** — cross-process live monitoring (recorder and viewer can be separate services).
368
+ - **Postgres** — durable + queryable for production, with `LISTEN/NOTIFY` live updates.
369
+
370
+ ## Audit & retention
371
+
372
+ ```python
373
+ from datetime import timedelta
374
+ from ft.retention import RetentionPolicy, purge_before
375
+ from ft.audit import engagement_digest, verify
376
+
377
+ # retention — drop whole completed engagements past their window (active ones never purged)
378
+ purged_ids = RetentionPolicy(max_age=timedelta(days=90)).apply(store, now=...)
379
+ # or: purge_before(store, cutoff_datetime)
380
+
381
+ # tamper-evidence — fingerprint an engagement; later, detect any alteration
382
+ digest = engagement_digest(store.get_engagement(eid)) # store this when the engagement ends
383
+ verify(store.get_engagement(eid), digest) # -> False if the trail was altered
384
+ ```
385
+
386
+ Anchor the digest outside the trace store (WORM / transparency log / signature) for strong
387
+ tamper-evidence — see `FlowTraicer/audit.py` for the threat model.
388
+
389
+ ## Analytics: funnels & journeys
390
+
391
+ Across many engagements (tag each with `user_id`/`session_id` in `metadata`), answer
392
+ *"where do users drop off, and what did each step cost?"*:
393
+
394
+ ```python
395
+ from ft.analytics import funnel, journeys, group_by
396
+
397
+ f = funnel(store, ["intake", "school_selection", "comparison", "application", "submitted"])
398
+ for step in f.steps:
399
+ print(step.name, "reached", step.reached, "dropped", step.dropped,
400
+ "conv", step.conversion_rate, "tokens", step.total_tokens,
401
+ "avg_ms", step.avg_duration_ms)
402
+
403
+ journeys(store, user_id="u-42") # all engagements for one user (filtered summaries)
404
+ group_by(store, "user_id") # {user_id: [summary, ...]}
405
+ store.list_engagements(where={"session_id": "s-1"}) # metadata-filtered index
406
+ ```
407
+
408
+ Drop-off is read from each engagement's recorded `dropped_at`, so the funnel stays correct
409
+ even when `order` contains an **optional** step (e.g. `comparison`) — `conversion_rate` is
410
+ always in `[0, 1]`.
411
+
412
+ ## Viewing traces
413
+
414
+ The viewer is a FastAPI app + a Cytoscape.js single page (graph on top, timeline below,
415
+ linked). To explore the bundled demo:
416
+
417
+ ```bash
418
+ python -m ft.server.app # http://127.0.0.1:8400
419
+ ```
420
+
421
+ To view **your own** store, build the app around it:
422
+
423
+ ```python
424
+ import uvicorn
425
+ from ft.server.app import create_app
426
+
427
+ uvicorn.run(create_app(SQLiteStore("traces.db")), host="127.0.0.1", port=8400)
428
+ ```
429
+
430
+ The API behind the viewer:
431
+
432
+ | Endpoint | Returns |
433
+ |---|---|
434
+ | `GET /api/engagements` | one summary row per engagement |
435
+ | `GET /api/engagements/{id}` | the full engagement tree (+ topology) |
436
+ | `GET /api/engagements/{id}/timeline` | the temporal viewmodel (lanes, marks) |
437
+ | `WS /api/stream` | records pushed live as they're appended |
438
+
439
+ ## Architecture
440
+
441
+ | Module | Responsibility |
442
+ |---|---|
443
+ | `ft.core.model` | the Pydantic data model (framework-agnostic) |
444
+ | `ft.store` | append-only `Store` protocol + SQLite default backend |
445
+ | `ft.recorder` | the fail-open emit contract |
446
+ | `ft.langgraph_adapter` | `run_instrumented` + `read_topology` |
447
+ | `ft.orchestration` | `Workflow` DSL — declare steps/global-steps/tools/goals over LangGraph |
448
+ | `ft.extraction` | Instructor-powered per-step schema extraction |
449
+ | `ft.llm` | config-driven multi-provider LLM calls (LiteLLM) |
450
+ | `ft.analytics` | cross-engagement funnels, drop-off, journey grouping |
451
+ | `ft.retention` / `ft.audit` | purge old engagements; tamper-evident digests |
452
+ | `ft.timeline` | temporal viewmodel for the timeline view |
453
+ | `ft.server` | FastAPI query + live-stream API and the viewer |
454
+
455
+ The trace **core knows nothing about LangGraph** — the adapter feeds it through the
456
+ recorder's small emit API, so other engines can be added as adapters later.
457
+
458
+ ## Status & roadmap
459
+
460
+ Done: trace core + SQLite/Redis/Postgres stores, LangGraph auto-instrumentation + the
461
+ `Workflow` DSL, Instructor extraction, config-driven LLM (LiteLLM), **per-step token cost**,
462
+ **goals/abandonment + drop-off analytics**, retention + tamper-evident audit, and the linked
463
+ graph/timeline viewer. Planned: OSS packaging. See [`docs/`](docs/).
464
+
465
+ ## License
466
+
467
+ Apache-2.0 (intended; final license confirmed at open-source time).
@@ -0,0 +1,36 @@
1
+ ft/__init__.py,sha256=qUrDzk92gLmGc9JvkAR5avQR_ipI69Ms0K5-9p2f5-A,115
2
+ ft/analytics.py,sha256=Mpck5KVzvcR3BoLHZYVzMVtmVkPEfGOZbj4RkQTvY7Y,4395
3
+ ft/audit.py,sha256=mqwiG_anwDcaT-j9ZotzSh8o2MyPb4YS-SBi4htsZEA,1325
4
+ ft/extraction.py,sha256=dq36JMv9fSzhPqcFMWpo65SmmonTp00ev0y-D2nXHhM,4416
5
+ ft/llm.py,sha256=jRpE7CDR37vTXJCTdskvUj7V3nLBnb7C19L3SK0Zuqg,6660
6
+ ft/orchestration.py,sha256=thkUbsWW4eWqv3gxPvisdmqdIm1KUwEJ9wlifAxtMXo,9272
7
+ ft/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ ft/recorder.py,sha256=lK42Rg5oaMBOCLf9Jf1QGUUA0nMmwmPX8UJO6tzDEkA,5924
9
+ ft/registry.py,sha256=cqVsbacosyeeNkFrDxz0dRnv83l-jOLZFbwKdM82lBM,6098
10
+ ft/retention.py,sha256=2J1SF1EkbraaUOZ5wZ0n0kOWL2qKWi-kck-Jkto7fWQ,1248
11
+ ft/timeline.py,sha256=bWcK51Miz90UqpTMRbyeKD0xH5_9vbcPuGgqgrPpfew,3462
12
+ ft/core/__init__.py,sha256=kqJgOMttZuK-BOSSMr31vVp1W2u2rW7vqGrzNxO2mag,53
13
+ ft/core/model.py,sha256=7us47Hc2Wg6JAq83IrwpgZiz8DcBAy62e6kKZExRltI,5050
14
+ ft/examples/__init__.py,sha256=QYjNJLbHwFqFuK73o2LhOd01sOcvdspsyUeMyjSZqDQ,79
15
+ ft/examples/demo_agent.py,sha256=3Ps0xrudDGw5uWJ8KDUX7BwSUhqAgIMFysQvGy2ZZYY,4228
16
+ ft/langgraph_adapter/__init__.py,sha256=8Z1-z50-MKlpt2TXzwPHQHVHPL2iSvtV2RgmDsTZsLk,252
17
+ ft/langgraph_adapter/runner.py,sha256=sSp-ypt_LI9gMrdiP_oRpzZDZear-cXI18n3g6xaXWY,6374
18
+ ft/langgraph_adapter/state.py,sha256=fWPaHXruHBfxylaEAwlYEP97au-Gmecd0MWMj6kV4sM,1480
19
+ ft/langgraph_adapter/topology.py,sha256=QB4p-3tf1DyL8DOGG5M3MG8cnkGyaiq9EwQ3Hu6Dv_4,1319
20
+ ft/server/__init__.py,sha256=h0sCdoKHP_7HaInZmZcfm_nst10bLW78BYLRRtgxf0Y,61
21
+ ft/server/app.py,sha256=NfbfQJkkYeLd-pFtpSmQZiktOkafGoR1dGgcWlaIbUY,3306
22
+ ft/server/static/app.js,sha256=SeO9txxdY4LVsirnT8hzFQgj1wf_Ykjf9OsnSmfPFyU,8730
23
+ ft/server/static/index.html,sha256=WU3WTD-MRbc8nyVvzIb_li20T66-GmG42S3edzD830I,1500
24
+ ft/server/static/style.css,sha256=JZin8ZQUlvJ51cGgbQLbpkmYStLits98E2RILiFFPO4,5684
25
+ ft/store/__init__.py,sha256=btEGrL3rcM0hsFSgyZ83JOgBdPd-YoL0bOsMTdRHlaA,70
26
+ ft/store/base.py,sha256=KAWlhChNiqXsIeBx5rs0b2cbCNpcjKb45xg-6lBuV08,2518
27
+ ft/store/postgres.py,sha256=ODPEFigjswtpK4wJTAZv4JARsu57FyewWIVC-H8oYA4,4890
28
+ ft/store/reconstruct.py,sha256=IMOUF21OWWWeQ_bvcoNqRX2ZanrBYW9Es8SsnhMmeyc,2455
29
+ ft/store/records.py,sha256=czyjNTJmeaRWexvvEwCQZGLBatlgYL3F1KwgdrHdAf0,2959
30
+ ft/store/redis.py,sha256=6njivSHWZPKr7U6NUMri6Rt2DfrekZbupviI0LlhTtU,4776
31
+ ft/store/sqlite.py,sha256=jLPq--Sn04mu7xcAZiiaVhbytmxWmotHdNzzy28u09g,4447
32
+ flowtraicer-0.9.0.dist-info/METADATA,sha256=FRYZVQi65yv8oPDWDmfD8Kp89_99ftGX64H76cTPQ9k,18993
33
+ flowtraicer-0.9.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
34
+ flowtraicer-0.9.0.dist-info/entry_points.txt,sha256=GpNtA3K4LrPJtFF7wNNVAaQ9Q9zeEKbOGfcHHFl1AsM,49
35
+ flowtraicer-0.9.0.dist-info/licenses/LICENSE,sha256=M1nqEbO93gnUfwN0DiV83O-7GgFhh4y3kqpYY-XYMmk,11357
36
+ flowtraicer-0.9.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ft-server = ft.server.app:main