fora-sdk 0.1.7__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.
@@ -0,0 +1,3 @@
1
+ dist/
2
+ build/
3
+ *.egg-info/
fora_sdk-0.1.7/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2025, Zansat Technologies Private Limited
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: fora-sdk
3
+ Version: 0.1.7
4
+ Summary: Typed builder for Fora voice-AI workflows
5
+ Project-URL: Homepage, https://github.com/Phoraapp/fora
6
+ Project-URL: Repository, https://github.com/Phoraapp/fora
7
+ Author-email: Phoraapp <phoraapp@gmail.com>
8
+ License: BSD-2-Clause
9
+ License-File: LICENSE
10
+ Keywords: agent,fora,llm,sdk,voice-ai,workflow
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: BSD License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: httpx>=0.27
22
+ Requires-Dist: pydantic>=2.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7.0; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # fora-sdk
28
+
29
+ Typed builder for Fora voice-AI workflows. Fetches the node-spec catalog from
30
+ the Fora backend at session start, validates every call against it at the
31
+ call site, and produces `ReactFlowDTO`-compatible JSON.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install fora-sdk
37
+ ```
38
+
39
+ For local development against a checked-out monorepo:
40
+
41
+ ```bash
42
+ pip install -e sdk/python/
43
+ ```
44
+
45
+ ## Usage
46
+
47
+ ```python
48
+ from fora_sdk import ForaClient, Workflow
49
+
50
+ with ForaClient(base_url="http://localhost:8000", api_key="...") as client:
51
+ wf = Workflow(client=client, name="loan_qualification")
52
+
53
+ start = wf.add(
54
+ type="startCall",
55
+ name="greeting",
56
+ prompt="You are Sarah from Acme Loans. Greet the caller warmly.",
57
+ greeting_type="text",
58
+ greeting="Hi {{first_name}}, this is Sarah.",
59
+ )
60
+ qualify = wf.add(
61
+ type="agentNode",
62
+ name="qualify",
63
+ prompt="Ask about loan amount and timeline.",
64
+ )
65
+ done = wf.add(type="endCall", name="done", prompt="Thank the caller.")
66
+
67
+ wf.edge(start, qualify, label="interested", condition="Caller expressed interest.")
68
+ wf.edge(qualify, done, label="done", condition="Qualification complete.")
69
+
70
+ client.save_workflow(workflow_id=123, workflow=wf)
71
+ ```
72
+
73
+ ## What gets validated at the call site
74
+
75
+ The SDK fetches the spec for each node type via `get_node_type` and raises
76
+ `ValidationError` immediately when:
77
+
78
+ - an unknown field is passed (catches typos)
79
+ - a required field is missing or empty
80
+ - a scalar type is wrong (e.g., string for a boolean)
81
+ - an `options` value isn't in the allowed list
82
+
83
+ When a spec carries an `llm_hint`, the hint is appended to the error message so
84
+ an LLM agent can self-correct on retry:
85
+
86
+ ```
87
+ tool_uuids: expected tool_refs, got str
88
+ Hint: List of tool UUIDs from `list_tools`.
89
+ ```
90
+
91
+ Server-side Pydantic validators run on save and surface anything the SDK lets
92
+ through (compound invariants, cross-field rules).
93
+
94
+ ## Environment
95
+
96
+ ```bash
97
+ FORA_API_URL=http://localhost:8000 # default (legacy DOGRAH_API_URL also accepted)
98
+ FORA_API_KEY=sk-... # sent as X-API-Key (legacy DOGRAH_API_KEY also accepted)
99
+ ```
100
+
101
+ ## License
102
+
103
+ BSD 2-Clause — see `LICENSE`.
@@ -0,0 +1,77 @@
1
+ # fora-sdk
2
+
3
+ Typed builder for Fora voice-AI workflows. Fetches the node-spec catalog from
4
+ the Fora backend at session start, validates every call against it at the
5
+ call site, and produces `ReactFlowDTO`-compatible JSON.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install fora-sdk
11
+ ```
12
+
13
+ For local development against a checked-out monorepo:
14
+
15
+ ```bash
16
+ pip install -e sdk/python/
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```python
22
+ from fora_sdk import ForaClient, Workflow
23
+
24
+ with ForaClient(base_url="http://localhost:8000", api_key="...") as client:
25
+ wf = Workflow(client=client, name="loan_qualification")
26
+
27
+ start = wf.add(
28
+ type="startCall",
29
+ name="greeting",
30
+ prompt="You are Sarah from Acme Loans. Greet the caller warmly.",
31
+ greeting_type="text",
32
+ greeting="Hi {{first_name}}, this is Sarah.",
33
+ )
34
+ qualify = wf.add(
35
+ type="agentNode",
36
+ name="qualify",
37
+ prompt="Ask about loan amount and timeline.",
38
+ )
39
+ done = wf.add(type="endCall", name="done", prompt="Thank the caller.")
40
+
41
+ wf.edge(start, qualify, label="interested", condition="Caller expressed interest.")
42
+ wf.edge(qualify, done, label="done", condition="Qualification complete.")
43
+
44
+ client.save_workflow(workflow_id=123, workflow=wf)
45
+ ```
46
+
47
+ ## What gets validated at the call site
48
+
49
+ The SDK fetches the spec for each node type via `get_node_type` and raises
50
+ `ValidationError` immediately when:
51
+
52
+ - an unknown field is passed (catches typos)
53
+ - a required field is missing or empty
54
+ - a scalar type is wrong (e.g., string for a boolean)
55
+ - an `options` value isn't in the allowed list
56
+
57
+ When a spec carries an `llm_hint`, the hint is appended to the error message so
58
+ an LLM agent can self-correct on retry:
59
+
60
+ ```
61
+ tool_uuids: expected tool_refs, got str
62
+ Hint: List of tool UUIDs from `list_tools`.
63
+ ```
64
+
65
+ Server-side Pydantic validators run on save and surface anything the SDK lets
66
+ through (compound invariants, cross-field rules).
67
+
68
+ ## Environment
69
+
70
+ ```bash
71
+ FORA_API_URL=http://localhost:8000 # default (legacy DOGRAH_API_URL also accepted)
72
+ FORA_API_KEY=sk-... # sent as X-API-Key (legacy DOGRAH_API_KEY also accepted)
73
+ ```
74
+
75
+ ## License
76
+
77
+ BSD 2-Clause — see `LICENSE`.
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "fora-sdk"
3
+ version = "0.1.7"
4
+ description = "Typed builder for Fora voice-AI workflows"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "BSD-2-Clause" }
8
+ authors = [
9
+ { name = "Phoraapp", email = "phoraapp@gmail.com" },
10
+ ]
11
+ keywords = ["fora", "voice-ai", "workflow", "sdk", "llm", "agent"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: BSD License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Topic :: Software Development :: Libraries",
22
+ ]
23
+ dependencies = [
24
+ "httpx>=0.27",
25
+ "pydantic>=2.0",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=7.0",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/Phoraapp/fora"
35
+ Repository = "https://github.com/Phoraapp/fora"
36
+
37
+ [project.scripts]
38
+ fora-sdk-codegen = "fora_sdk.codegen:main"
39
+
40
+ [build-system]
41
+ requires = ["hatchling"]
42
+ build-backend = "hatchling.build"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/fora_sdk"]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
@@ -0,0 +1,35 @@
1
+ """Fora SDK — typed builder for voice-AI workflows.
2
+
3
+ Runtime SDK: fetches the spec catalog from the Fora backend at session
4
+ start and validates every `Workflow.add()` call against it. LLMs don't
5
+ need to import per-node-type classes — the `type` argument is a string
6
+ keyed against the fetched spec catalog.
7
+
8
+ from fora_sdk import ForaClient, Workflow
9
+
10
+ with ForaClient(base_url="http://localhost:8000", api_key=...) as client:
11
+ wf = Workflow(client=client, name="loan_qualification")
12
+ start = wf.add(type="startCall", name="greeting", prompt="...")
13
+ qualify = wf.add(type="agentNode", name="qualify", prompt="...")
14
+ wf.edge(start, qualify, label="interested", condition="...")
15
+ client.save_workflow(workflow_id=123, workflow=wf)
16
+
17
+ For typed IDE autocomplete, generate per-node dataclasses via the SDK
18
+ codegen (Phase 6) — the runtime and typed SDKs share this same core.
19
+ """
20
+
21
+ from .client import ForaClient
22
+ from .errors import ApiError, ForaSdkError, SpecMismatchError, ValidationError
23
+ from .typed._base import TypedNode
24
+ from .workflow import NodeRef, Workflow
25
+
26
+ __all__ = [
27
+ "ApiError",
28
+ "ForaClient",
29
+ "ForaSdkError",
30
+ "NodeRef",
31
+ "SpecMismatchError",
32
+ "TypedNode",
33
+ "ValidationError",
34
+ "Workflow",
35
+ ]
@@ -0,0 +1,114 @@
1
+ """GENERATED — do not edit. Source: filtered OpenAPI from `api.app`.
2
+
3
+ Regenerate with `./scripts/generate_sdk.sh`.
4
+
5
+ `ForaClient` mixes in this class to get HTTP methods for every route
6
+ decorated with `sdk_expose(...)` on the backend. Request/response types
7
+ come from `_generated_models` (datamodel-codegen output).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from fora_sdk._generated_models import (
15
+ CreateToolRequest,
16
+ CreateWorkflowRequest,
17
+ CredentialResponse,
18
+ DocumentListResponseSchema,
19
+ InitiateCallRequest,
20
+ NodeSpec,
21
+ NodeTypesResponse,
22
+ RecordingListResponseSchema,
23
+ ToolResponse,
24
+ UpdateWorkflowRequest,
25
+ WorkflowListResponse,
26
+ WorkflowResponse,
27
+ )
28
+
29
+
30
+ class _GeneratedClient:
31
+ # `ForaClient.__init__` installs `self._request` (see client.py).
32
+
33
+ def create_tool(self, *, body: CreateToolRequest) -> ToolResponse:
34
+ """Create a reusable tool for the authenticated organization."""
35
+ data = self._request("POST", "/tools/", json=body.model_dump(mode="json", exclude_none=True))
36
+ return ToolResponse.model_validate(data)
37
+
38
+ def create_workflow(self, *, body: CreateWorkflowRequest) -> WorkflowResponse:
39
+ """Create a new workflow from a workflow definition."""
40
+ data = self._request("POST", "/workflow/create/definition", json=body.model_dump(mode="json", exclude_none=True))
41
+ return WorkflowResponse.model_validate(data)
42
+
43
+ def get_node_type(self, name: str) -> NodeSpec:
44
+ """Fetch a single node spec by name."""
45
+ data = self._request("GET", f"/node-types/{name}")
46
+ return NodeSpec.model_validate(data)
47
+
48
+ def get_workflow(self, workflow_id: int) -> WorkflowResponse:
49
+ """Get a single workflow by ID (returns draft if one exists, else published)."""
50
+ data = self._request("GET", f"/workflow/fetch/{workflow_id}")
51
+ return WorkflowResponse.model_validate(data)
52
+
53
+ def list_credentials(self) -> list[CredentialResponse]:
54
+ """List webhook credentials available to the authenticated organization."""
55
+ data = self._request("GET", "/credentials/")
56
+ return [CredentialResponse.model_validate(x) for x in data]
57
+
58
+ def list_documents(self, *, status: str | None = None, limit: int | None = None, offset: int | None = None) -> DocumentListResponseSchema:
59
+ """List knowledge base documents available to the authenticated organization."""
60
+ params: dict[str, Any] = {}
61
+ if status is not None:
62
+ params["status"] = status
63
+ if limit is not None:
64
+ params["limit"] = limit
65
+ if offset is not None:
66
+ params["offset"] = offset
67
+ data = self._request("GET", "/knowledge-base/documents", params=params)
68
+ return DocumentListResponseSchema.model_validate(data)
69
+
70
+ def list_node_types(self) -> NodeTypesResponse:
71
+ """List every registered node type with its spec. Pinned to spec_version."""
72
+ data = self._request("GET", "/node-types")
73
+ return NodeTypesResponse.model_validate(data)
74
+
75
+ def list_recordings(self, *, workflow_id: int | None = None, tts_provider: str | None = None, tts_model: str | None = None, tts_voice_id: str | None = None) -> RecordingListResponseSchema:
76
+ """List workflow recordings available to the authenticated organization."""
77
+ params: dict[str, Any] = {}
78
+ if workflow_id is not None:
79
+ params["workflow_id"] = workflow_id
80
+ if tts_provider is not None:
81
+ params["tts_provider"] = tts_provider
82
+ if tts_model is not None:
83
+ params["tts_model"] = tts_model
84
+ if tts_voice_id is not None:
85
+ params["tts_voice_id"] = tts_voice_id
86
+ data = self._request("GET", "/workflow-recordings/", params=params)
87
+ return RecordingListResponseSchema.model_validate(data)
88
+
89
+ def list_tools(self, *, status: str | None = None, category: str | None = None) -> list[ToolResponse]:
90
+ """List tools available to the authenticated organization."""
91
+ params: dict[str, Any] = {}
92
+ if status is not None:
93
+ params["status"] = status
94
+ if category is not None:
95
+ params["category"] = category
96
+ data = self._request("GET", "/tools/", params=params)
97
+ return [ToolResponse.model_validate(x) for x in data]
98
+
99
+ def list_workflows(self, *, status: str | None = None) -> list[WorkflowListResponse]:
100
+ """List all workflows in the authenticated organization."""
101
+ params: dict[str, Any] = {}
102
+ if status is not None:
103
+ params["status"] = status
104
+ data = self._request("GET", "/workflow/fetch", params=params)
105
+ return [WorkflowListResponse.model_validate(x) for x in data]
106
+
107
+ def test_phone_call(self, *, body: InitiateCallRequest) -> Any:
108
+ """Place a test call from a workflow to a phone number."""
109
+ return self._request("POST", "/telephony/initiate-call", json=body.model_dump(mode="json", exclude_none=True))
110
+
111
+ def update_workflow(self, workflow_id: int, *, body: UpdateWorkflowRequest) -> WorkflowResponse:
112
+ """Update a workflow's name and/or definition. Saves as a new draft."""
113
+ data = self._request("PUT", f"/workflow/{workflow_id}", json=body.model_dump(mode="json", exclude_none=True))
114
+ return WorkflowResponse.model_validate(data)