labs-agent-lib 0.1.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,16 @@
1
+ from .logger import get_logger
2
+ from .agent_settings import get_agent_settings
3
+ from .file_reader.extractor import extract_text
4
+ from .file_writer import file_writer
5
+ from .agent_environment import get_agent_environment
6
+ from .observability import AgentTranscriptWriter, PromptLogWriter
7
+
8
+ __all__ = [
9
+ "get_logger",
10
+ "get_agent_settings",
11
+ "extract_text",
12
+ "file_writer",
13
+ "get_agent_environment",
14
+ "AgentTranscriptWriter",
15
+ "PromptLogWriter",
16
+ ]
@@ -0,0 +1,12 @@
1
+ from labs_agent_lib.agent_settings import AgentSettings, LogLevel, Provider, get_agent_settings
2
+
3
+ from .agent_environment import AgentEnvironment, get_agent_environment
4
+
5
+ __all__ = [
6
+ "AgentEnvironment",
7
+ "AgentSettings",
8
+ "LogLevel",
9
+ "Provider",
10
+ "get_agent_environment",
11
+ "get_agent_settings",
12
+ ]
@@ -0,0 +1,55 @@
1
+ """Builds the Claude Agent SDK environment for the gateway configured in AgentSettings."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from labs_agent_lib.agent_settings import AgentSettings, Provider
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class AgentEnvironment:
10
+ """What ClaudeAgentOptions needs to reach a model gateway.
11
+
12
+ Attributes:
13
+ env: Environment overrides for ClaudeAgentOptions.env; the SDK merges this
14
+ over the inherited environment before spawning the `claude` subprocess.
15
+ The model itself is set in code, not here.
16
+ """
17
+
18
+ env: dict[str, str]
19
+
20
+
21
+ def get_agent_environment(settings: AgentSettings) -> AgentEnvironment:
22
+ """Build the AgentEnvironment for the provider configured in settings.provider.
23
+
24
+ Both supported providers speak the Anthropic Messages API, so routing to either
25
+ one is just a matter of pointing ANTHROPIC_BASE_URL/ANTHROPIC_API_KEY at it.
26
+ """
27
+ if settings.provider is Provider.PORTKEY:
28
+ base_url, api_key = settings.portkey_base_url, settings.portkey_api_key
29
+ else:
30
+ base_url, api_key = settings.anthropic_base_url, settings.anthropic_api_key
31
+
32
+ if not api_key.get_secret_value():
33
+ raise ValueError(
34
+ f"LABS_AGENT_LIB_{settings.provider.value.upper()}_API_KEY is required when provider={settings.provider}"
35
+ )
36
+
37
+ if settings.provider is Provider.PORTKEY:
38
+ headers = f"x-portkey-api-key: {api_key.get_secret_value()}"
39
+ if settings.portkey_virtual:
40
+ headers += f"\nx-portkey-virtual-key: {settings.portkey_virtual}"
41
+ env = {
42
+ "ANTHROPIC_BASE_URL": base_url,
43
+ "ANTHROPIC_API_KEY": api_key.get_secret_value(),
44
+ "ANTHROPIC_CUSTOM_HEADERS": headers,
45
+ }
46
+ else:
47
+ env = {
48
+ "ANTHROPIC_BASE_URL": base_url,
49
+ "ANTHROPIC_API_KEY": api_key.get_secret_value(),
50
+ }
51
+
52
+ if settings.disable_prompt_caching:
53
+ env["DISABLE_PROMPT_CACHING"] = "1"
54
+
55
+ return AgentEnvironment(env=env)
@@ -0,0 +1,62 @@
1
+ """Application settings for projects built on labs-agent-lib.
2
+
3
+ Consuming projects configure this via a `.env` file (or real env vars) at their own
4
+ repository root, prefixing every key with `LABS_AGENT_LIB_`, e.g.:
5
+
6
+ LABS_AGENT_LIB_PROVIDER=anthropic
7
+ LABS_AGENT_LIB_LOG_LEVEL=DEBUG
8
+ LABS_AGENT_LIB_ANTHROPIC_API_KEY=sk-ant-...
9
+
10
+ Call `get_agent_settings()` to get the cached singleton instead of instantiating
11
+ `AgentSettings` directly, so the `.env` file is only read once per process.
12
+ """
13
+
14
+ from enum import StrEnum
15
+ from functools import lru_cache
16
+
17
+ from pydantic import SecretStr
18
+ from pydantic_settings import BaseSettings, SettingsConfigDict
19
+
20
+
21
+ class Provider(StrEnum):
22
+ """Anthropic-Messages-API-compatible gateways this agent can route through."""
23
+
24
+ PORTKEY = "portkey"
25
+ ANTHROPIC = "anthropic"
26
+
27
+
28
+ class LogLevel(StrEnum):
29
+ """Verbosity for labs_resume_agent.libs.logger - maps 1:1 to stdlib logging levels."""
30
+
31
+ DEBUG = "DEBUG"
32
+ INFO = "INFO"
33
+ WARNING = "WARNING"
34
+ ERROR = "ERROR"
35
+
36
+
37
+ class AgentSettings(BaseSettings):
38
+ model_config = SettingsConfigDict(
39
+ env_file=".env",
40
+ env_prefix="LABS_AGENT_LIB_",
41
+ env_file_encoding="utf-8",
42
+ extra="ignore",
43
+ populate_by_name=True,
44
+ )
45
+
46
+ provider: Provider = Provider.PORTKEY
47
+ log_level: LogLevel = LogLevel.INFO
48
+
49
+ portkey_base_url: str = "https://portkeygateway.perficient.com"
50
+ portkey_api_key: SecretStr = SecretStr("")
51
+ portkey_virtual: str = ""
52
+
53
+ anthropic_base_url: str = "https://api.anthropic.com"
54
+ anthropic_api_key: SecretStr = SecretStr("")
55
+
56
+ disable_prompt_caching: bool = False
57
+
58
+
59
+ @lru_cache
60
+ def get_agent_settings() -> AgentSettings:
61
+ """Return the cached application settings singleton."""
62
+ return AgentSettings()
@@ -0,0 +1,8 @@
1
+ from .errors import UnsupportedFileTypeError
2
+ from .extractor import SUPPORTED_EXTENSIONS, extract_text
3
+
4
+ __all__ = [
5
+ "SUPPORTED_EXTENSIONS",
6
+ "UnsupportedFileTypeError",
7
+ "extract_text",
8
+ ]
@@ -0,0 +1,9 @@
1
+ """Extracts text from Word (.docx) files."""
2
+
3
+ import docx2txt
4
+ from pathlib import Path
5
+
6
+ def read_docx(path: Path) -> str:
7
+ """Extrae todo el texto de un DOCX, incluyendo tablas."""
8
+ text = docx2txt.process(path)
9
+ return text.strip()
@@ -0,0 +1,13 @@
1
+ """Errors raised by the file_reader library."""
2
+
3
+
4
+ class UnsupportedFileTypeError(ValueError):
5
+ """Raised when a file's extension has no registered reader."""
6
+
7
+ def __init__(self, extension: str, supported: list[str]) -> None:
8
+ self.extension = extension
9
+ self.supported = supported
10
+ super().__init__(
11
+ f"Unsupported file type '{extension or '(none)'}'. "
12
+ f"Supported extensions: {', '.join(supported)}"
13
+ )
@@ -0,0 +1,35 @@
1
+ """Picks a reader for a document from its extension and extracts its text."""
2
+
3
+ from pathlib import Path
4
+
5
+ from .docx_reader import read_docx
6
+ from .errors import UnsupportedFileTypeError
7
+ from .pdf_reader import read_pdf
8
+ from .text_reader import read_text_file
9
+
10
+ _READERS = {
11
+ ".pdf": read_pdf,
12
+ ".docx": read_docx,
13
+ ".txt": read_text_file,
14
+ ".md": read_text_file,
15
+ ".markdown": read_text_file,
16
+ }
17
+
18
+ SUPPORTED_EXTENSIONS = sorted(_READERS)
19
+
20
+
21
+ def extract_text(file_path: str) -> str:
22
+ """Read `file_path` and return its text content.
23
+
24
+ The reader is picked from the file's extension (see SUPPORTED_EXTENSIONS);
25
+ callers don't need to know which parser handles which format.
26
+ """
27
+ path = Path(file_path)
28
+ if not path.is_file():
29
+ raise FileNotFoundError(f"File not found: {path}")
30
+
31
+ read = _READERS.get(path.suffix.lower())
32
+ if read is None:
33
+ raise UnsupportedFileTypeError(path.suffix, SUPPORTED_EXTENSIONS)
34
+
35
+ return read(path)
@@ -0,0 +1,11 @@
1
+ """Extracts text from PDF files."""
2
+
3
+
4
+ import pymupdf
5
+ from pathlib import Path
6
+
7
+ def read_pdf(path: Path) -> str:
8
+ """Return the concatenated text of every page in a PDF file."""
9
+ doc = pymupdf.open(path)
10
+ pages = [page.get_text("text") for page in doc]
11
+ return "\n\n".join(pages).strip()
@@ -0,0 +1,7 @@
1
+ """Extracts text from plain-text files (.txt, .md, and similar)."""
2
+
3
+ from pathlib import Path
4
+
5
+ def read_text_file(path: Path) -> str:
6
+ """Return the file's contents decoded as UTF-8."""
7
+ return path.read_text(encoding="utf-8", errors="replace").strip()
@@ -0,0 +1,7 @@
1
+ """Utilities for writing/moving resume files on disk."""
2
+
3
+ from .resume_mover import file_writer
4
+
5
+ __all__ = [
6
+ "file_writer",
7
+ ]
@@ -0,0 +1,13 @@
1
+ """Moves resume files into their target folder."""
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+
7
+ def file_writer(source_file_path: str, target_path: str) -> None:
8
+ """Moves `source_file_path` into `target_path`, creating the folder if needed."""
9
+ source = Path(source_file_path)
10
+ target_dir = Path(target_path)
11
+ target_dir.mkdir(parents=True, exist_ok=True)
12
+
13
+ shutil.move(source_file_path, target_dir / source.name)
@@ -0,0 +1,37 @@
1
+ """Centralized logging setup for the whole app (coordinators, subagent task events, MCP tools).
2
+
3
+ Usage: `logger = get_logger(__name__)` at module level, then `logger.debug/info/warning/error(...)`.
4
+ Verbosity is controlled by AgentSettings.log_level (LABS_RESUME_AGENT_LOG_LEVEL) - set it to DEBUG to
5
+ see every step (tool calls, per-task progress) or leave it at INFO to only see the high-level
6
+ phases (task started/done, final results). Logs go to stderr, not stdout: mcp_server.py runs as
7
+ an MCP stdio subprocess, and anything printed to stdout there would corrupt the MCP protocol.
8
+ """
9
+
10
+ import logging
11
+ import sys
12
+
13
+ from labs_agent_lib.agent_settings import get_agent_settings
14
+
15
+ _configured_roots: set[str] = set()
16
+
17
+
18
+ def _configure(root_name: str) -> None:
19
+ if root_name in _configured_roots:
20
+ return
21
+ root = logging.getLogger(root_name)
22
+ root.setLevel(get_agent_settings().log_level.value)
23
+ handler = logging.StreamHandler(sys.stderr)
24
+ handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)-8s %(name)s: %(message)s"))
25
+ root.addHandler(handler)
26
+ root.propagate = False
27
+ _configured_roots.add(root_name)
28
+
29
+
30
+ def get_logger(name: str) -> logging.Logger:
31
+ """Return a logger in the caller's package hierarchy; pass __name__ from the call site.
32
+
33
+ The top-level package segment of `name` is configured as its own logging root on first
34
+ use, so each consuming project gets its level/handler without hardcoding its package name.
35
+ """
36
+ _configure(name.split(".", 1)[0])
37
+ return logging.getLogger(name)
@@ -0,0 +1,11 @@
1
+ """Shared prompt-log / agent-transcript JSONL writers for Claude-agent-based projects.
2
+
3
+ Each consuming project supplies its own settings object (just needs
4
+ `observability_enabled`, `prompt_log_dir`, `agent_transcript_dir`) and a `service` name;
5
+ see `PromptLogWriter`/`AgentTranscriptWriter` for the full contract.
6
+ """
7
+
8
+ from .agent_transcript import AgentTranscriptWriter
9
+ from .prompt_log import PromptLogWriter
10
+
11
+ __all__ = ["AgentTranscriptWriter", "PromptLogWriter"]
@@ -0,0 +1,31 @@
1
+ """Shared JSONL append helper backing the prompt log and agent transcript writers."""
2
+
3
+ from datetime import UTC, datetime
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ def append_jsonl(directory: str, file_stem: str, record: dict[str, Any]) -> None:
10
+ """Appends one JSON line to `{directory}/{file_stem}.jsonl`, creating the directory if needed."""
11
+ path = Path(directory)
12
+ path.mkdir(parents=True, exist_ok=True)
13
+ line = json.dumps(record, ensure_ascii=False, default=str)
14
+ with (path / f"{file_stem}.jsonl").open("a", encoding="utf-8") as handle:
15
+ handle.write(line + "\n")
16
+
17
+
18
+ def utc_now_iso() -> str:
19
+ """Returns the current UTC time as an ISO-8601 string."""
20
+ return datetime.now(UTC).isoformat()
21
+
22
+
23
+ def file_stem_for(correlation_id: str, agent: str | None = None) -> str:
24
+ """Builds the JSONL file stem for one `correlation_id`, optionally keyed by agent too.
25
+
26
+ Pass `agent` when several agents can write concurrently under the same
27
+ `correlation_id` (e.g. parallel sub-agent tool calls fanned out by a coordinator in
28
+ one turn), so each gets its own file instead of interleaving writes into a shared one.
29
+ """
30
+ base = correlation_id or "no-correlation-id"
31
+ return f"{base}__{agent}" if agent else base
@@ -0,0 +1,159 @@
1
+ """Agent transcript: the full, ordered play-by-play of one agent session or tool call.
2
+
3
+ Complements the prompt log (a flat index of what was asked). See a consuming project's
4
+ `documentation/prompt_log_and_agent_transcript.md` for the full design and how
5
+ `correlation_id` ties multiple services together.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from ._jsonl_writer import append_jsonl, file_stem_for, utc_now_iso
11
+
12
+
13
+ class AgentTranscriptWriter:
14
+ """Appends one event line per step of an agent run to `{agent_transcript_dir}/{file_stem}.jsonl`.
15
+
16
+ `settings` only needs `observability_enabled: bool` and `agent_transcript_dir: str`
17
+ attributes - any project's own settings object works, no shared schema required.
18
+ `service` names the calling project in every record (e.g. `"mcp_skills_extractor_agents"`).
19
+ Pass `key_by_agent=True` when multiple agents can write under the same
20
+ `correlation_id` concurrently, so each gets its own file instead of interleaving
21
+ writes - e.g. several extraction sub-agents fanned out in parallel by a coordinator.
22
+
23
+ Methods cover the union of events either a coordinator (`session_start`,
24
+ `assistant_text`, `tool_use`, `tool_result`, `session_end`) or a self-correcting
25
+ sub-agent (`session_start`, `assistant_text`, `retry`, `session_end`) needs. Callers
26
+ only use the subset that applies to them.
27
+ """
28
+
29
+ def __init__(self, settings: Any, *, service: str, key_by_agent: bool = False) -> None:
30
+ self.settings = settings
31
+ self.service = service
32
+ self.key_by_agent = key_by_agent
33
+
34
+ def _write(
35
+ self,
36
+ correlation_id: str,
37
+ agent: str,
38
+ event: str,
39
+ *,
40
+ session_id: str | None = None,
41
+ **fields: Any,
42
+ ) -> None:
43
+ if not self.settings.observability_enabled:
44
+ return
45
+
46
+ record: dict[str, Any] = {
47
+ "timestamp": utc_now_iso(),
48
+ "correlation_id": correlation_id,
49
+ "service": self.service,
50
+ "agent": agent,
51
+ "session_id": session_id,
52
+ "event": event,
53
+ **fields,
54
+ }
55
+ file_stem = file_stem_for(correlation_id, agent if self.key_by_agent else None)
56
+ append_jsonl(self.settings.agent_transcript_dir, file_stem, record)
57
+
58
+ def session_start(
59
+ self,
60
+ correlation_id: str,
61
+ agent: str,
62
+ *,
63
+ model: str,
64
+ system_prompt: str,
65
+ user_prompt: str,
66
+ allowed_tools: list[str] | None = None,
67
+ ) -> None:
68
+ """Records the opening of a session/first attempt, including the system prompt once."""
69
+ self._write(
70
+ correlation_id,
71
+ agent,
72
+ "session_start",
73
+ model=model,
74
+ system_prompt=system_prompt,
75
+ user_prompt=user_prompt,
76
+ allowed_tools=list(allowed_tools) if allowed_tools is not None else None,
77
+ )
78
+
79
+ def assistant_text(
80
+ self,
81
+ correlation_id: str,
82
+ agent: str,
83
+ *,
84
+ text: str,
85
+ session_id: str | None = None,
86
+ ) -> None:
87
+ """Records a text block/raw response the agent emitted."""
88
+ self._write(correlation_id, agent, "assistant_text", text=text, session_id=session_id)
89
+
90
+ def tool_use(
91
+ self,
92
+ correlation_id: str,
93
+ agent: str,
94
+ *,
95
+ tool_use_id: str,
96
+ tool_name: str,
97
+ target_field: str,
98
+ tool_input: Any,
99
+ ) -> None:
100
+ """Records a tool call the agent dispatched."""
101
+ self._write(
102
+ correlation_id,
103
+ agent,
104
+ "tool_use",
105
+ tool_use_id=tool_use_id,
106
+ tool_name=tool_name,
107
+ target_field=target_field,
108
+ tool_input=tool_input,
109
+ )
110
+
111
+ def tool_result(
112
+ self,
113
+ correlation_id: str,
114
+ agent: str,
115
+ *,
116
+ tool_use_id: str,
117
+ tool_name: str,
118
+ target_field: str,
119
+ is_error: bool,
120
+ output: str,
121
+ ) -> None:
122
+ """Records the result that came back for a previously dispatched tool call."""
123
+ self._write(
124
+ correlation_id,
125
+ agent,
126
+ "tool_result",
127
+ tool_use_id=tool_use_id,
128
+ tool_name=tool_name,
129
+ target_field=target_field,
130
+ is_error=is_error,
131
+ output=output,
132
+ )
133
+
134
+ def retry(self, correlation_id: str, agent: str, *, attempt: int, attempts: int, error: str) -> None:
135
+ """Records a self-correction turn triggered by a schema/validation failure."""
136
+ self._write(correlation_id, agent, "retry", attempt=attempt, attempts=attempts, error=error)
137
+
138
+ def session_end(
139
+ self,
140
+ correlation_id: str,
141
+ agent: str,
142
+ *,
143
+ is_error: bool,
144
+ session_id: str | None = None,
145
+ stop_reason: str | None = None,
146
+ result: Any = None,
147
+ error: str | None = None,
148
+ ) -> None:
149
+ """Records the final outcome of the session/tool call."""
150
+ self._write(
151
+ correlation_id,
152
+ agent,
153
+ "session_end",
154
+ session_id=session_id,
155
+ stop_reason=stop_reason,
156
+ is_error=is_error,
157
+ result=result,
158
+ error=error,
159
+ )
@@ -0,0 +1,55 @@
1
+ """Prompt log: one audit line per outbound LLM request attempt - "what we asked the model".
2
+
3
+ Complements the agent transcript (the full play-by-play of a session). The prompt log is
4
+ a flat, append-only index meant for quick search/filtering across runs; see a consuming
5
+ project's `documentation/prompt_log_and_agent_transcript.md` for the full design and how
6
+ `correlation_id` ties multiple services together.
7
+ """
8
+
9
+ from typing import Any
10
+
11
+ from ._jsonl_writer import append_jsonl, file_stem_for, utc_now_iso
12
+
13
+
14
+ class PromptLogWriter:
15
+ """Appends one line per LLM call attempt to `{prompt_log_dir}/{file_stem}.jsonl`.
16
+
17
+ `settings` only needs `observability_enabled: bool` and `prompt_log_dir: str`
18
+ attributes - any project's own settings object works, no shared schema required.
19
+ `service` names the calling project in every record (e.g. `"talent_distiller_agent"`).
20
+ Pass `key_by_agent=True` when multiple agents can log under the same `correlation_id`
21
+ concurrently, so each gets its own file instead of interleaving writes.
22
+ """
23
+
24
+ def __init__(self, settings: Any, *, service: str, key_by_agent: bool = False) -> None:
25
+ self.settings = settings
26
+ self.service = service
27
+ self.key_by_agent = key_by_agent
28
+
29
+ def log_prompt(
30
+ self,
31
+ *,
32
+ correlation_id: str,
33
+ agent: str,
34
+ model: str,
35
+ system_prompt_name: str,
36
+ user_prompt: str,
37
+ attempt: int = 1,
38
+ ) -> None:
39
+ """Records one outbound prompt. No-ops when `settings.observability_enabled` is `False`."""
40
+ if not self.settings.observability_enabled:
41
+ return
42
+
43
+ record: dict[str, Any] = {
44
+ "timestamp": utc_now_iso(),
45
+ "correlation_id": correlation_id,
46
+ "service": self.service,
47
+ "agent": agent,
48
+ "event": "prompt",
49
+ "model": model,
50
+ "system_prompt_name": system_prompt_name,
51
+ "user_prompt": user_prompt,
52
+ "attempt": attempt,
53
+ }
54
+ file_stem = file_stem_for(correlation_id, agent if self.key_by_agent else None)
55
+ append_jsonl(self.settings.prompt_log_dir, file_stem, record)
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: labs-agent-lib
3
+ Version: 0.1.0
4
+ Summary: Shared utilities (settings, logging, document text extraction, Claude Agent SDK environment) for Claude-agent-based projects.
5
+ Author: Luis Alberto Bolaños Salazar
6
+ Author-email: Luis Alberto Bolaños Salazar <68343185+albertbolanoss@users.noreply.github.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Dist: docx2txt>=0.9
10
+ Requires-Dist: pydantic>=2.14.1
11
+ Requires-Dist: pydantic-settings>=2.16.1
12
+ Requires-Dist: pymupdf>=2.0
13
+ Requires-Dist: python-dotenv>=1.2.4
14
+ Requires-Python: >=3.14
15
+ Description-Content-Type: text/markdown
16
+
17
+ # labs-agent-lib
18
+
19
+ Shared utilities extracted from [`candidate-match-agent`](https://github.com/albertbolanoss/candidate-match-agent) for
20
+ building Claude-agent-based projects:
21
+
22
+ - `labs_agent_lib.agent_settings` — a `pydantic-settings` `AgentSettings` model (gateway provider, log level, Portkey/Anthropic
23
+ credentials, prompt-caching toggle).
24
+ - `labs_agent_lib.logger` — centralized stderr logging, configured from `AgentSettings.log_level`.
25
+ - `labs_agent_lib.file_reader` — extracts plain text from `.pdf`, `.docx`, `.txt`, `.md`, and `.markdown` files.
26
+ - `labs_agent_lib.agent_environment` — builds the `ClaudeAgentOptions.env` dict (base URL, API key, custom headers)
27
+ for the configured provider.
28
+ - `labs_agent_lib.observability` — `PromptLogWriter`/`AgentTranscriptWriter`: JSONL writers for a prompt log
29
+ ("what we asked the model") and an agent transcript ("what happened, step by step"), correlated by a
30
+ caller-supplied `correlation_id`. Each consuming project supplies its own settings object (just needs
31
+ `observability_enabled`, `prompt_log_dir`, `agent_transcript_dir`) and a `service` name.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ uv add labs-agent-lib
37
+ # or
38
+ pip install labs-agent-lib
39
+ ```
40
+
41
+ ## Configuration
42
+
43
+ `AgentSettings` reads from your project's own `.env` file (or real environment variables), with every key
44
+ prefixed `LABS_AGENT_LIB_`:
45
+
46
+ | Env var | Default | Description |
47
+ | --- | --- | --- |
48
+ | `LABS_AGENT_LIB_PROVIDER` | `portkey` | Gateway to route through: `portkey` or `anthropic`. |
49
+ | `LABS_AGENT_LIB_LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, or `ERROR`. |
50
+ | `LABS_AGENT_LIB_PORTKEY_BASE_URL` | `https://portkeygateway.perficient.com` | Portkey gateway base URL. |
51
+ | `LABS_AGENT_LIB_PORTKEY_API_KEY` | `""` | Portkey API key (secret). |
52
+ | `LABS_AGENT_LIB_PORTKEY_VIRTUAL` | `""` | Provider/credential alias from the Portkey dashboard (e.g. `aws-bedrock-use2`). |
53
+ | `LABS_AGENT_LIB_ANTHROPIC_BASE_URL` | `https://api.anthropic.com` | Anthropic API base URL. |
54
+ | `LABS_AGENT_LIB_ANTHROPIC_API_KEY` | `""` | Anthropic API key (secret). |
55
+ | `LABS_AGENT_LIB_DISABLE_PROMPT_CACHING` | `false` | Disable Anthropic prompt caching, e.g. while iterating on a system prompt. |
56
+
57
+ Example `.env`:
58
+
59
+ ```dotenv
60
+ LABS_AGENT_LIB_PROVIDER=anthropic
61
+ LABS_AGENT_LIB_ANTHROPIC_API_KEY=sk-ant-...
62
+ ```
63
+
64
+ ## Usage
65
+
66
+ ```python
67
+ from labs_agent_lib import get_logger, get_agent_settings, get_agent_environment
68
+ from labs_agent_lib.file_reader import extract_text
69
+
70
+ logger = get_logger(__name__)
71
+ settings = get_agent_settings()
72
+ env = get_agent_environment(settings)
73
+ text = extract_text("resume.pdf")
74
+ ```
75
+
76
+ ## Development
77
+
78
+ ```bash
79
+ uv sync
80
+ uv run pytest
81
+ ```
@@ -0,0 +1,21 @@
1
+ labs_agent_lib/__init__.py,sha256=itGrZzRqTvUlxwEkddnJVBs-NNfB6dc0n2pV2HNZQxM,475
2
+ labs_agent_lib/agent_environment/__init__.py,sha256=RqUGGPuQemne07DTBONw8_rKkJ4bK42_JiYa6l6OMK4,327
3
+ labs_agent_lib/agent_environment/agent_environment.py,sha256=FUJLmoTUQKqNEAcozQlwm4rwWI_gh5M8BlJrCdgcfkI,2066
4
+ labs_agent_lib/agent_settings.py,sha256=KqWHUhgAfwNIB1TBAH1yPriGFUBHS7MWKC_mUQRjUPA,1838
5
+ labs_agent_lib/file_reader/__init__.py,sha256=o1Hvsys1Ex74IRKGJ6e6T8MQHQflyhwuGHMtl1DwxGg,198
6
+ labs_agent_lib/file_reader/docx_reader.py,sha256=XiUjfadt7Z4wbfWJjVtLlV8_SMmJg8v8-ZkxmRa5PuA,251
7
+ labs_agent_lib/file_reader/errors.py,sha256=iHtLVT2Tu2yr0FV3cY4MAFkJvKVSQvUhodRDDu3XgzM,463
8
+ labs_agent_lib/file_reader/extractor.py,sha256=_4aUf_TDdSZik_n-cxsGFku9qT0mnWknOLkSo3coC3A,1011
9
+ labs_agent_lib/file_reader/pdf_reader.py,sha256=vOLRGO3AxY_2-wtNPFJbB1O5diTeuGCfSKGNjZzAr_I,308
10
+ labs_agent_lib/file_reader/text_reader.py,sha256=rQJSLG8DEH3vp-9oBSqMfXXqDFJoe0ZIo8sRzY9QIGA,266
11
+ labs_agent_lib/file_writer/__init__.py,sha256=d_Rcex8-RuJWAuHNV2qf1tVzGOGLoU31srhodlDChRE,137
12
+ labs_agent_lib/file_writer/resume_mover.py,sha256=DnUv0ZySzPjIuCVD_GDHDWM63w35afw90l4nNhIPAZs,440
13
+ labs_agent_lib/logger.py,sha256=NQW791rUg5_4p_F1lze0Qs5mHQya7rlqgMeAyUSttTw,1588
14
+ labs_agent_lib/observability/__init__.py,sha256=6IDRJEpHljjZiuV8NGpsD6qvU57dSsaEjz067tVTczw,478
15
+ labs_agent_lib/observability/_jsonl_writer.py,sha256=uoRuCte8GrLuymS4TcqTgXzcBvhXWbwQnZDsStH2xXI,1303
16
+ labs_agent_lib/observability/agent_transcript.py,sha256=SzCUOkQ5ASRYRRC2JdryogK6ovHzCqBsOavenQ7DpQw,5386
17
+ labs_agent_lib/observability/prompt_log.py,sha256=dkVz-qrwEkBLn4q4Pi4Bx5m5NoMOJv1syapesfWlewE,2229
18
+ labs_agent_lib-0.1.0.dist-info/licenses/LICENSE,sha256=IGiwkyXXQSAmCP3-U_MxnvtRsVwAdr_QE8JtR_gYW40,1107
19
+ labs_agent_lib-0.1.0.dist-info/WHEEL,sha256=q9Wah2_CgP24mRNSlTyb0F8Yrbz55SzVl8eHLkZ_wvE,80
20
+ labs_agent_lib-0.1.0.dist-info/METADATA,sha256=cwf5b63xRwOoR5HCubO5wQxUR4rNVFeyAEuOSeym0xo,3317
21
+ labs_agent_lib-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.2
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Luis Alberto Bolaños Salazar
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.