callin 0.1.0__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,5 @@
1
+ __pycache__/
2
+ *.pyc
3
+ dist/
4
+ node_modules/
5
+ .env
callin-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Replacing Humans LLC (Callin.io)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
callin-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.5
2
+ Name: callin
3
+ Version: 0.1.0
4
+ Summary: Give your AI agent a real phone line: Callin.io voice agents, calls and transcripts for Python, LangChain and CrewAI.
5
+ Project-URL: Homepage, https://callin.io
6
+ Project-URL: Documentation, https://support.callin.io/api-reference/introduction
7
+ Project-URL: Source, https://github.com/callin-io/callin-agents
8
+ Author-email: "Callin.io" <hello@callin.io>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai agents,callin,crewai,langchain,mcp,phone calls,telephony,voice ai
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Communications :: Telephony
15
+ Requires-Python: >=3.9
16
+ Requires-Dist: httpx>=0.24
17
+ Provides-Extra: crewai
18
+ Requires-Dist: crewai>=0.80; extra == 'crewai'
19
+ Provides-Extra: langchain
20
+ Requires-Dist: langchain-core>=0.2; extra == 'langchain'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Callin.io for AI agents
24
+
25
+ **Give your AI agent a real phone line.** Callin.io places phone calls with an AI voice agent and returns what happened: status, summary and transcript. Calls in Italian, Spanish, Portuguese, English, French, German, Dutch and Hindi, GDPR compliant, and every call discloses that it is an AI.
26
+
27
+ | Use it from | How |
28
+ |---|---|
29
+ | Claude, ChatGPT, Cursor, VS Code, OpenAI Agents SDK | MCP server `https://claude-mcp.callin.io/mcp` (OAuth) — registry name `io.callin/callin` |
30
+ | Python | `pip install callin` |
31
+ | LangChain | `pip install "callin[langchain]"` → `from callin.langchain import get_callin_tools` |
32
+ | CrewAI | `pip install "callin[crewai]"` → `from callin.crewai import CallinPlaceCallTool` |
33
+ | Node.js | `npm i callin-sdk` |
34
+ | OpenClaw / Claude skills | [`skills/callin-phone/SKILL.md`](skills/callin-phone/SKILL.md) |
35
+ | n8n | community node `n8n-nodes-callin` |
36
+ | REST | [API reference](https://support.callin.io/api-reference/introduction) · [OpenAPI](https://support.callin.io/openapi.json) |
37
+
38
+ ## Quickstart (Python)
39
+
40
+ ```python
41
+ from callin import Callin # env: CALLIN_API_KEY, CALLIN_AGENT_ID
42
+
43
+ result = Callin().place_call(
44
+ "+390111234567",
45
+ objective="Book a table for 2 tomorrow at 20:30 under the name Rossi",
46
+ language="it",
47
+ wait=True,
48
+ )
49
+ print(result.status, result.summary)
50
+ ```
51
+
52
+ ## Quickstart (Node)
53
+
54
+ ```js
55
+ import { Callin } from "callin-sdk";
56
+ const callin = new Callin(); // env: CALLIN_API_KEY, CALLIN_AGENT_ID
57
+ const { campaignId } = await callin.placeCall({ to: "+34911234567", objective: "Ask for the first free appointment next week", language: "es" });
58
+ console.log(await callin.waitForCall(campaignId));
59
+ ```
60
+
61
+ ## How it works
62
+ 1. Create a voice agent in [app.callin.io](https://app.callin.io) with a phone number, using the [Task Caller template](task-caller-agent/prompt.md). Its id is `CALLIN_AGENT_ID`.
63
+ 2. `place_call` starts a one-contact campaign for that agent and passes `objective`, `context` and `language` as dynamic variables.
64
+ 3. `get_call_result` / `wait_for_call` returns status, summary and transcript when the call ends.
65
+
66
+ ## Responsible use
67
+ Call only when your user asks you to, and only numbers they give you or public business numbers. No unsolicited marketing, no emergency numbers, no payment card data over the phone. The agent always states that it is an AI.
68
+
69
+ ## Pricing
70
+ $0.16 per minute, all inclusive (telephony, language model and voice), no monthly fee. See [callin.io/pricing](https://callin.io/pricing).
71
+
72
+ MIT licensed. © Replacing Humans LLC — Callin.io.
callin-0.1.0/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # Callin.io for AI agents
2
+
3
+ **Give your AI agent a real phone line.** Callin.io places phone calls with an AI voice agent and returns what happened: status, summary and transcript. Calls in Italian, Spanish, Portuguese, English, French, German, Dutch and Hindi, GDPR compliant, and every call discloses that it is an AI.
4
+
5
+ | Use it from | How |
6
+ |---|---|
7
+ | Claude, ChatGPT, Cursor, VS Code, OpenAI Agents SDK | MCP server `https://claude-mcp.callin.io/mcp` (OAuth) — registry name `io.callin/callin` |
8
+ | Python | `pip install callin` |
9
+ | LangChain | `pip install "callin[langchain]"` → `from callin.langchain import get_callin_tools` |
10
+ | CrewAI | `pip install "callin[crewai]"` → `from callin.crewai import CallinPlaceCallTool` |
11
+ | Node.js | `npm i callin-sdk` |
12
+ | OpenClaw / Claude skills | [`skills/callin-phone/SKILL.md`](skills/callin-phone/SKILL.md) |
13
+ | n8n | community node `n8n-nodes-callin` |
14
+ | REST | [API reference](https://support.callin.io/api-reference/introduction) · [OpenAPI](https://support.callin.io/openapi.json) |
15
+
16
+ ## Quickstart (Python)
17
+
18
+ ```python
19
+ from callin import Callin # env: CALLIN_API_KEY, CALLIN_AGENT_ID
20
+
21
+ result = Callin().place_call(
22
+ "+390111234567",
23
+ objective="Book a table for 2 tomorrow at 20:30 under the name Rossi",
24
+ language="it",
25
+ wait=True,
26
+ )
27
+ print(result.status, result.summary)
28
+ ```
29
+
30
+ ## Quickstart (Node)
31
+
32
+ ```js
33
+ import { Callin } from "callin-sdk";
34
+ const callin = new Callin(); // env: CALLIN_API_KEY, CALLIN_AGENT_ID
35
+ const { campaignId } = await callin.placeCall({ to: "+34911234567", objective: "Ask for the first free appointment next week", language: "es" });
36
+ console.log(await callin.waitForCall(campaignId));
37
+ ```
38
+
39
+ ## How it works
40
+ 1. Create a voice agent in [app.callin.io](https://app.callin.io) with a phone number, using the [Task Caller template](task-caller-agent/prompt.md). Its id is `CALLIN_AGENT_ID`.
41
+ 2. `place_call` starts a one-contact campaign for that agent and passes `objective`, `context` and `language` as dynamic variables.
42
+ 3. `get_call_result` / `wait_for_call` returns status, summary and transcript when the call ends.
43
+
44
+ ## Responsible use
45
+ Call only when your user asks you to, and only numbers they give you or public business numbers. No unsolicited marketing, no emergency numbers, no payment card data over the phone. The agent always states that it is an AI.
46
+
47
+ ## Pricing
48
+ $0.16 per minute, all inclusive (telephony, language model and voice), no monthly fee. See [callin.io/pricing](https://callin.io/pricing).
49
+
50
+ MIT licensed. © Replacing Humans LLC — Callin.io.
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "callin"
7
+ version = "0.1.0"
8
+ description = "Give your AI agent a real phone line: Callin.io voice agents, calls and transcripts for Python, LangChain and CrewAI."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Callin.io", email = "hello@callin.io" }]
13
+ keywords = ["ai agents", "phone calls", "voice ai", "telephony", "langchain", "crewai", "mcp", "callin"]
14
+ dependencies = ["httpx>=0.24"]
15
+ classifiers = ["Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Topic :: Communications :: Telephony"]
16
+
17
+ [project.optional-dependencies]
18
+ langchain = ["langchain-core>=0.2"]
19
+ crewai = ["crewai>=0.80"]
20
+
21
+ [project.urls]
22
+ Homepage = "https://callin.io"
23
+ Documentation = "https://support.callin.io/api-reference/introduction"
24
+ Source = "https://github.com/callin-io/callin-agents"
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["src/callin"]
@@ -0,0 +1,5 @@
1
+ """Callin.io for AI agents: place real phone calls and read call results."""
2
+ from .client import Callin, CallinError, CallResult
3
+
4
+ __all__ = ["Callin", "CallinError", "CallResult"]
5
+ __version__ = "0.1.0"
@@ -0,0 +1,238 @@
1
+ """Minimal client for the Callin.io public API (https://support.callin.io/api-reference/introduction).
2
+
3
+ Phone calls are placed through a Callin.io voice agent that has a phone number.
4
+ `place_call` creates a one-contact campaign for that agent and passes the call goal
5
+ as dynamic variables (`objective`, `context`, ...), which the agent's prompt uses.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import time
11
+ import uuid
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Dict, List, Optional
14
+
15
+ import httpx
16
+
17
+ DEFAULT_BASE_URL = "https://api.callin.io/api"
18
+ FINAL_STATUSES = {"completed", "done", "ended", "failed", "no-answer", "no_answer", "busy", "voicemail", "canceled", "cancelled", "error"}
19
+
20
+
21
+ class CallinError(RuntimeError):
22
+ """Raised when the Callin.io API returns an error."""
23
+
24
+ def __init__(self, message: str, status: Optional[int] = None, details: Any = None):
25
+ super().__init__(message)
26
+ self.status = status
27
+ self.details = details
28
+
29
+
30
+ @dataclass
31
+ class CallResult:
32
+ """Outcome of a phone call placed with `Callin.place_call`."""
33
+
34
+ campaign_id: str
35
+ status: str
36
+ finished: bool
37
+ to: str
38
+ call_id: Optional[str] = None
39
+ conversation_id: Optional[str] = None
40
+ duration_seconds: Optional[int] = None
41
+ summary: Optional[str] = None
42
+ transcript: Optional[str] = None
43
+ voicemail: Optional[bool] = None
44
+ error: Optional[str] = None
45
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
46
+
47
+ def to_dict(self) -> Dict[str, Any]:
48
+ d = {k: v for k, v in self.__dict__.items() if k != "raw"}
49
+ return d
50
+
51
+
52
+ class Callin:
53
+ """Client for the Callin.io public API.
54
+
55
+ Args:
56
+ api_key: Callin.io API key. Defaults to the ``CALLIN_API_KEY`` environment variable.
57
+ agent_id: Default voice agent used by :meth:`place_call`. Defaults to ``CALLIN_AGENT_ID``.
58
+ base_url: API base URL.
59
+ """
60
+
61
+ def __init__(self, api_key: Optional[str] = None, agent_id: Optional[str] = None,
62
+ base_url: str = DEFAULT_BASE_URL, timeout: float = 30.0):
63
+ self.api_key = api_key or os.environ.get("CALLIN_API_KEY")
64
+ if not self.api_key:
65
+ raise CallinError("Missing API key: pass api_key= or set CALLIN_API_KEY")
66
+ self.agent_id = agent_id or os.environ.get("CALLIN_AGENT_ID")
67
+ self._http = httpx.Client(base_url=base_url.rstrip("/"), timeout=timeout,
68
+ headers={"x-api-key": self.api_key, "user-agent": "callin-python/0.1.0"})
69
+
70
+ # ---- low level -------------------------------------------------------
71
+ def _request(self, method: str, path: str, **kw) -> Any:
72
+ r = self._http.request(method, path, **kw)
73
+ try:
74
+ body = r.json()
75
+ except ValueError:
76
+ raise CallinError(f"HTTP {r.status_code}: {r.text[:200]}", r.status_code)
77
+ if r.status_code >= 400 or body.get("success") is False:
78
+ err = body.get("error") or {}
79
+ msg = err.get("message") if isinstance(err, dict) else str(err)
80
+ raise CallinError(msg or f"HTTP {r.status_code}", r.status_code, body)
81
+ return body.get("data", body)
82
+
83
+ def close(self) -> None:
84
+ self._http.close()
85
+
86
+ def __enter__(self):
87
+ return self
88
+
89
+ def __exit__(self, *exc):
90
+ self.close()
91
+
92
+ # ---- account & agents ------------------------------------------------
93
+ def me(self) -> Dict[str, Any]:
94
+ """Return the user id and email of the API key's account."""
95
+ return self._request("GET", "/public/v1/me")
96
+
97
+ def list_agents(self, search: Optional[str] = None, page_size: int = 20) -> List[Dict[str, Any]]:
98
+ params = {"pageSize": page_size}
99
+ if search:
100
+ params["search"] = search
101
+ return self._request("GET", "/public/v1/agents", params=params)
102
+
103
+ def get_agent(self, agent_id: str) -> Dict[str, Any]:
104
+ return self._request("GET", f"/public/v1/agents/{agent_id}")
105
+
106
+ # ---- conversations ---------------------------------------------------
107
+ def list_conversations(self, agent_id: Optional[str] = None, page_size: int = 20,
108
+ start_after_unix: Optional[int] = None, status: Optional[str] = None) -> List[Dict[str, Any]]:
109
+ params: Dict[str, Any] = {"pageSize": page_size}
110
+ if agent_id:
111
+ params["agentId"] = agent_id
112
+ if start_after_unix:
113
+ params["startAfterUnix"] = start_after_unix
114
+ if status:
115
+ params["status"] = status
116
+ return self._request("GET", "/public/v1/conversations", params=params)
117
+
118
+ def get_conversation(self, conversation_id: str) -> Dict[str, Any]:
119
+ return self._request("GET", f"/public/v1/conversations/{conversation_id}")
120
+
121
+ # ---- phone calls -----------------------------------------------------
122
+ def place_call(self, to: str, objective: str, *, context: str = "", language: str = "",
123
+ agent_id: Optional[str] = None, contact_name: str = "",
124
+ variables: Optional[Dict[str, Any]] = None, wait: bool = False,
125
+ timeout_seconds: int = 600, poll_seconds: int = 10) -> CallResult:
126
+ """Place a phone call to ``to`` (E.164, e.g. "+390111234567") with a goal.
127
+
128
+ The goal and context are passed to the voice agent as dynamic variables
129
+ ``objective``, ``context`` and ``language``. Use an agent whose prompt
130
+ includes ``{{objective}}`` and ``{{context}}`` (see the "Task Caller" template).
131
+
132
+ With ``wait=True`` the method blocks until the call ends and returns the
133
+ summary and transcript; otherwise call :meth:`get_call_result` later.
134
+ """
135
+ agent = agent_id or self.agent_id
136
+ if not agent:
137
+ raise CallinError("Missing agent: pass agent_id= or set CALLIN_AGENT_ID")
138
+ if not to.startswith("+"):
139
+ raise CallinError("Phone number must be in E.164 format, e.g. +390111234567")
140
+ contact: Dict[str, Any] = {"phoneNumber": to, "objective": objective}
141
+ if contact_name:
142
+ contact["name"] = contact_name
143
+ if context:
144
+ contact["context"] = context
145
+ if language:
146
+ contact["language"] = language
147
+ for k, v in (variables or {}).items():
148
+ contact[k] = v
149
+ name = f"agent-call {time.strftime('%Y-%m-%d %H:%M:%S')} {uuid.uuid4().hex[:6]}"
150
+ camp = self._request("POST", "/public/v1/campaigns",
151
+ json={"name": name, "agentId": agent, "contacts": [contact], "launch": True})
152
+ result = CallResult(campaign_id=camp.get("campaignId"), status=camp.get("status") or "queued",
153
+ finished=False, to=to, raw={"campaign": camp})
154
+ if wait:
155
+ return self.wait_for_call(result.campaign_id, timeout_seconds=timeout_seconds, poll_seconds=poll_seconds)
156
+ return result
157
+
158
+ def get_call_result(self, campaign_id: str) -> CallResult:
159
+ """Return the current status and, once finished, the summary and transcript of a call."""
160
+ camp = self._request("GET", f"/public/v1/campaigns/{campaign_id}")
161
+ calls = self._request("GET", f"/public/v1/campaigns/{campaign_id}/calls", params={"pageSize": 5})
162
+ to = ""
163
+ contacts = camp.get("contacts") or []
164
+ if contacts:
165
+ to = contacts[0].get("phoneNumber", "")
166
+ if not calls:
167
+ cstatus = (camp.get("status") or "queued").lower()
168
+ res = CallResult(campaign_id=campaign_id, status=cstatus, finished=cstatus in FINAL_STATUSES,
169
+ to=to, raw={"campaign": camp})
170
+ if res.finished:
171
+ created = (camp.get("metadata") or {}).get("createdAt")
172
+ conv = self._find_conversation(camp.get("agentId"), {"contactNumber": to, "startedAt": created})
173
+ if conv:
174
+ self._fill(res, conv)
175
+ return res
176
+ call = calls[0]
177
+ status = (call.get("status") or "").lower() or "in_progress"
178
+ res = CallResult(campaign_id=campaign_id, status=status, finished=status in FINAL_STATUSES, to=to,
179
+ call_id=call.get("callId"), duration_seconds=call.get("durationSeconds"),
180
+ error=call.get("errorMessage") or None, raw={"campaign": camp, "call": call})
181
+ if res.finished:
182
+ conv = self._find_conversation(camp.get("agentId"), call)
183
+ if conv:
184
+ self._fill(res, conv)
185
+ return res
186
+
187
+ @staticmethod
188
+ def _fill(res: "CallResult", conv: Dict[str, Any]) -> None:
189
+ res.conversation_id = conv.get("conversationId")
190
+ res.summary = conv.get("summary")
191
+ res.transcript = conv.get("transcript")
192
+ res.voicemail = conv.get("voiceMailDetected")
193
+ res.duration_seconds = res.duration_seconds or conv.get("conversationDuration")
194
+ res.raw["conversation"] = conv
195
+
196
+ def wait_for_call(self, campaign_id: str, timeout_seconds: int = 600, poll_seconds: int = 10) -> CallResult:
197
+ deadline = time.time() + timeout_seconds
198
+ res = self.get_call_result(campaign_id)
199
+ while not res.finished and time.time() < deadline:
200
+ time.sleep(poll_seconds)
201
+ res = self.get_call_result(campaign_id)
202
+ return res
203
+
204
+ def _find_conversation(self, agent_id: Optional[str], call: Dict[str, Any]) -> Optional[Dict[str, Any]]:
205
+ call_id = call.get("callId")
206
+ if call_id:
207
+ try:
208
+ conv = self.get_conversation(call_id)
209
+ conv.setdefault("conversationId", call_id)
210
+ return conv
211
+ except CallinError:
212
+ pass
213
+ started = call.get("startedAt")
214
+ after = None
215
+ if started:
216
+ try:
217
+ from datetime import datetime
218
+ after = int(datetime.fromisoformat(started.replace("Z", "+00:00")).timestamp()) - 120
219
+ except ValueError:
220
+ after = None
221
+ number = (call.get("contactNumber") or "").replace(" ", "")
222
+ convs = self.list_conversations(agent_id=agent_id, page_size=20, start_after_unix=after)
223
+ match = None
224
+ if number:
225
+ match = next((c for c in convs if number in (c.get("contactNumber") or "") or number in (c.get("callerNumber") or "")), None)
226
+ if match is None and after:
227
+ # A dedicated "task caller" agent places one call per campaign: take the first outbound call after launch.
228
+ outbound = [c for c in convs if (c.get("direction") or "outbound") == "outbound" and (c.get("startTimeUnix") or 0) >= after]
229
+ outbound.sort(key=lambda c: c.get("startTimeUnix") or 0)
230
+ match = outbound[0] if outbound else None
231
+ if match is None:
232
+ return None
233
+ try:
234
+ conv = self.get_conversation(match["conversationId"])
235
+ conv.setdefault("conversationId", match["conversationId"])
236
+ return conv
237
+ except CallinError:
238
+ return match
@@ -0,0 +1,48 @@
1
+ """CrewAI tools for Callin.io. Install with: pip install "callin[crewai]"."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import Optional, Type
6
+
7
+ from crewai.tools import BaseTool
8
+ from pydantic import BaseModel, Field
9
+
10
+ from .client import Callin
11
+
12
+
13
+ class _PlaceCallInput(BaseModel):
14
+ to: str = Field(..., description="Phone number in E.164 format, e.g. +390111234567")
15
+ objective: str = Field(..., description="What the call must achieve, in plain language")
16
+ context: str = Field("", description="Facts the caller may share")
17
+ language: str = Field("", description='Language code for the call, e.g. "it", "es", "pt", "en"')
18
+
19
+
20
+ class CallinPlaceCallTool(BaseTool):
21
+ name: str = "callin_place_call"
22
+ description: str = ("Place a real phone call with an AI voice agent that discloses it is an AI. "
23
+ "Returns campaign_id; use callin_get_call_result to read the outcome.")
24
+ args_schema: Type[BaseModel] = _PlaceCallInput
25
+ client: Optional[Callin] = None
26
+
27
+ model_config = {"arbitrary_types_allowed": True}
28
+
29
+ def _run(self, to: str, objective: str, context: str = "", language: str = "") -> str:
30
+ c = self.client or Callin()
31
+ return json.dumps(c.place_call(to=to, objective=objective, context=context, language=language).to_dict())
32
+
33
+
34
+ class _ResultInput(BaseModel):
35
+ campaign_id: str = Field(..., description="campaign_id returned by callin_place_call")
36
+
37
+
38
+ class CallinGetCallResultTool(BaseTool):
39
+ name: str = "callin_get_call_result"
40
+ description: str = "Get the status, summary and transcript of a call placed with callin_place_call."
41
+ args_schema: Type[BaseModel] = _ResultInput
42
+ client: Optional[Callin] = None
43
+
44
+ model_config = {"arbitrary_types_allowed": True}
45
+
46
+ def _run(self, campaign_id: str) -> str:
47
+ c = self.client or Callin()
48
+ return json.dumps(c.get_call_result(campaign_id).to_dict())
@@ -0,0 +1,44 @@
1
+ """LangChain tools for Callin.io. Install with: pip install "callin[langchain]"."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import List, Optional
6
+
7
+ from langchain_core.tools import BaseTool, tool
8
+
9
+ from .client import Callin
10
+
11
+
12
+ def get_callin_tools(client: Optional[Callin] = None) -> List[BaseTool]:
13
+ """Return LangChain tools that let an agent place phone calls and read results."""
14
+ c = client or Callin()
15
+
16
+ @tool("callin_place_call")
17
+ def place_call(to: str, objective: str, context: str = "", language: str = "") -> str:
18
+ """Place a real phone call with an AI voice agent.
19
+
20
+ Args:
21
+ to: Phone number in E.164 format, e.g. +390111234567.
22
+ objective: What the call must achieve, in plain language
23
+ (e.g. "Book a table for 2 tomorrow at 20:30 under the name Rossi").
24
+ context: Facts the caller may share (names, order numbers, callback number).
25
+ language: Language code for the call, e.g. "it", "es", "pt", "en".
26
+ Returns a JSON string with campaign_id and status. The call is asynchronous:
27
+ use callin_get_call_result with the campaign_id to read the outcome.
28
+ The voice agent always discloses that it is an AI assistant.
29
+ """
30
+ r = c.place_call(to=to, objective=objective, context=context, language=language)
31
+ return json.dumps(r.to_dict())
32
+
33
+ @tool("callin_get_call_result")
34
+ def get_call_result(campaign_id: str) -> str:
35
+ """Get the status of a phone call placed with callin_place_call and, when it has
36
+ finished, its summary and transcript. Returns a JSON string."""
37
+ return json.dumps(c.get_call_result(campaign_id).to_dict())
38
+
39
+ @tool("callin_list_recent_calls")
40
+ def list_recent_calls(limit: int = 10) -> str:
41
+ """List recent calls handled by your Callin.io voice agents, with summaries. Returns JSON."""
42
+ return json.dumps(c.list_conversations(page_size=limit))
43
+
44
+ return [place_call, get_call_result, list_recent_calls]