flashlite 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.
Files changed (41) hide show
  1. flashlite/__init__.py +169 -0
  2. flashlite/cache/__init__.py +14 -0
  3. flashlite/cache/base.py +194 -0
  4. flashlite/cache/disk.py +285 -0
  5. flashlite/cache/memory.py +157 -0
  6. flashlite/client.py +671 -0
  7. flashlite/config.py +154 -0
  8. flashlite/conversation/__init__.py +30 -0
  9. flashlite/conversation/context.py +319 -0
  10. flashlite/conversation/manager.py +385 -0
  11. flashlite/conversation/multi_agent.py +378 -0
  12. flashlite/core/__init__.py +13 -0
  13. flashlite/core/completion.py +145 -0
  14. flashlite/core/messages.py +130 -0
  15. flashlite/middleware/__init__.py +18 -0
  16. flashlite/middleware/base.py +90 -0
  17. flashlite/middleware/cache.py +121 -0
  18. flashlite/middleware/logging.py +159 -0
  19. flashlite/middleware/rate_limit.py +211 -0
  20. flashlite/middleware/retry.py +149 -0
  21. flashlite/observability/__init__.py +34 -0
  22. flashlite/observability/callbacks.py +155 -0
  23. flashlite/observability/inspect_compat.py +266 -0
  24. flashlite/observability/logging.py +293 -0
  25. flashlite/observability/metrics.py +221 -0
  26. flashlite/py.typed +0 -0
  27. flashlite/structured/__init__.py +31 -0
  28. flashlite/structured/outputs.py +189 -0
  29. flashlite/structured/schema.py +165 -0
  30. flashlite/templating/__init__.py +11 -0
  31. flashlite/templating/engine.py +217 -0
  32. flashlite/templating/filters.py +143 -0
  33. flashlite/templating/registry.py +165 -0
  34. flashlite/tools/__init__.py +74 -0
  35. flashlite/tools/definitions.py +382 -0
  36. flashlite/tools/execution.py +353 -0
  37. flashlite/types.py +233 -0
  38. flashlite-0.1.0.dist-info/METADATA +173 -0
  39. flashlite-0.1.0.dist-info/RECORD +41 -0
  40. flashlite-0.1.0.dist-info/WHEEL +4 -0
  41. flashlite-0.1.0.dist-info/licenses/LICENSE.md +21 -0
@@ -0,0 +1,143 @@
1
+ """Custom Jinja filters for prompt templating."""
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from jinja2 import Environment
7
+
8
+
9
+ def json_encode(value: Any, indent: int | None = None) -> str:
10
+ """
11
+ Encode a value as JSON string.
12
+
13
+ Usage in template: {{ data | json }}
14
+ """
15
+ return json.dumps(value, indent=indent, ensure_ascii=False)
16
+
17
+
18
+ def json_encode_pretty(value: Any) -> str:
19
+ """
20
+ Encode a value as pretty-printed JSON.
21
+
22
+ Usage in template: {{ data | json_pretty }}
23
+ """
24
+ return json.dumps(value, indent=2, ensure_ascii=False)
25
+
26
+
27
+ def truncate_words(value: str, max_words: int, suffix: str = "...") -> str:
28
+ """
29
+ Truncate string to a maximum number of words.
30
+
31
+ Usage in template: {{ text | truncate_words(100) }}
32
+ """
33
+ words = value.split()
34
+ if len(words) <= max_words:
35
+ return value
36
+ return " ".join(words[:max_words]) + suffix
37
+
38
+
39
+ def truncate_chars(value: str, max_chars: int, suffix: str = "...") -> str:
40
+ """
41
+ Truncate string to a maximum number of characters.
42
+
43
+ Usage in template: {{ text | truncate_chars(500) }}
44
+ """
45
+ if len(value) <= max_chars:
46
+ return value
47
+ return value[: max_chars - len(suffix)] + suffix
48
+
49
+
50
+ def escape_xml(value: str) -> str:
51
+ """
52
+ Escape XML special characters.
53
+
54
+ Usage in template: {{ text | escape_xml }}
55
+ """
56
+ replacements = [
57
+ ("&", "&amp;"),
58
+ ("<", "&lt;"),
59
+ (">", "&gt;"),
60
+ ('"', "&quot;"),
61
+ ("'", "&apos;"),
62
+ ]
63
+ result = value
64
+ for old, new in replacements:
65
+ result = result.replace(old, new)
66
+ return result
67
+
68
+
69
+ def strip_tags(value: str) -> str:
70
+ """
71
+ Remove XML/HTML tags from string.
72
+
73
+ Usage in template: {{ html_content | strip_tags }}
74
+ """
75
+ import re
76
+
77
+ return re.sub(r"<[^>]+>", "", value)
78
+
79
+
80
+ def bullet_list(items: list[str], bullet: str = "- ") -> str:
81
+ """
82
+ Format a list as bullet points.
83
+
84
+ Usage in template: {{ items | bullet_list }}
85
+ """
86
+ return "\n".join(f"{bullet}{item}" for item in items)
87
+
88
+
89
+ def numbered_list(items: list[str], start: int = 1) -> str:
90
+ """
91
+ Format a list as numbered items.
92
+
93
+ Usage in template: {{ items | numbered_list }}
94
+ """
95
+ return "\n".join(f"{i}. {item}" for i, item in enumerate(items, start=start))
96
+
97
+
98
+ def wrap_xml(value: str, tag: str) -> str:
99
+ """
100
+ Wrap content in XML tags.
101
+
102
+ Usage in template: {{ content | wrap_xml('context') }}
103
+ Produces: <context>content</context>
104
+ """
105
+ return f"<{tag}>{value}</{tag}>"
106
+
107
+
108
+ def indent_text(value: str, spaces: int = 2, first_line: bool = True) -> str:
109
+ """
110
+ Indent text by a number of spaces.
111
+
112
+ Usage in template: {{ text | indent_text(4) }}
113
+ """
114
+ prefix = " " * spaces
115
+ lines = value.split("\n")
116
+ if first_line:
117
+ return "\n".join(prefix + line for line in lines)
118
+ else:
119
+ return lines[0] + "\n" + "\n".join(prefix + line for line in lines[1:])
120
+
121
+
122
+ def default_if_empty(value: Any, default: Any) -> Any:
123
+ """
124
+ Return default if value is empty/falsy.
125
+
126
+ Usage in template: {{ maybe_empty | default_if_empty('N/A') }}
127
+ """
128
+ return value if value else default
129
+
130
+
131
+ def register_default_filters(env: Environment) -> None:
132
+ """Register all default filters with a Jinja environment."""
133
+ env.filters["json"] = json_encode
134
+ env.filters["json_pretty"] = json_encode_pretty
135
+ env.filters["truncate_words"] = truncate_words
136
+ env.filters["truncate_chars"] = truncate_chars
137
+ env.filters["escape_xml"] = escape_xml
138
+ env.filters["strip_tags"] = strip_tags
139
+ env.filters["bullet_list"] = bullet_list
140
+ env.filters["numbered_list"] = numbered_list
141
+ env.filters["wrap_xml"] = wrap_xml
142
+ env.filters["indent_text"] = indent_text
143
+ env.filters["default_if_empty"] = default_if_empty
@@ -0,0 +1,165 @@
1
+ """Template registry for managing prompt templates."""
2
+
3
+ import hashlib
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from jinja2 import Template
8
+
9
+ from ..types import TemplateError
10
+
11
+
12
+ class TemplateRegistry:
13
+ """
14
+ Registry for managing prompt templates.
15
+
16
+ Supports:
17
+ - Loading templates from files
18
+ - Registering templates by name
19
+ - Namespaced template names (e.g., 'prompts.system.default')
20
+ - Template versioning via content hashing
21
+ """
22
+
23
+ def __init__(self) -> None:
24
+ self._templates: dict[str, Template] = {}
25
+ self._sources: dict[str, str] = {} # name -> original source
26
+ self._hashes: dict[str, str] = {} # name -> content hash
27
+
28
+ def register(self, name: str, template: Template | str) -> None:
29
+ """
30
+ Register a template by name.
31
+
32
+ Args:
33
+ name: Template name (can use dots for namespacing)
34
+ template: Jinja Template object or template string
35
+ """
36
+ if isinstance(template, str):
37
+ source = template
38
+ template = Template(template)
39
+ else:
40
+ source = template.source if hasattr(template, "source") else ""
41
+
42
+ self._templates[name] = template
43
+ self._sources[name] = source
44
+ self._hashes[name] = self._compute_hash(source)
45
+
46
+ def get(self, name: str) -> Template:
47
+ """
48
+ Get a template by name.
49
+
50
+ Args:
51
+ name: Template name
52
+
53
+ Returns:
54
+ The Jinja Template
55
+
56
+ Raises:
57
+ TemplateError: If template not found
58
+ """
59
+ if name not in self._templates:
60
+ raise TemplateError(f"Template not found: {name}")
61
+ return self._templates[name]
62
+
63
+ def render(self, name: str, variables: dict[str, Any] | None = None) -> str:
64
+ """
65
+ Render a template by name.
66
+
67
+ Args:
68
+ name: Template name
69
+ variables: Variables to pass to template
70
+
71
+ Returns:
72
+ Rendered template string
73
+ """
74
+ template = self.get(name)
75
+ return template.render(**(variables or {}))
76
+
77
+ def has(self, name: str) -> bool:
78
+ """Check if a template exists."""
79
+ return name in self._templates
80
+
81
+ def list_templates(self, prefix: str | None = None) -> list[str]:
82
+ """
83
+ List registered template names.
84
+
85
+ Args:
86
+ prefix: Optional prefix to filter by (e.g., 'prompts.')
87
+
88
+ Returns:
89
+ List of template names
90
+ """
91
+ names = list(self._templates.keys())
92
+ if prefix:
93
+ names = [n for n in names if n.startswith(prefix)]
94
+ return sorted(names)
95
+
96
+ def get_hash(self, name: str) -> str:
97
+ """
98
+ Get the content hash of a template.
99
+
100
+ Useful for reproducibility tracking.
101
+ """
102
+ if name not in self._hashes:
103
+ raise TemplateError(f"Template not found: {name}")
104
+ return self._hashes[name]
105
+
106
+ def get_source(self, name: str) -> str:
107
+ """Get the original source of a template."""
108
+ if name not in self._sources:
109
+ raise TemplateError(f"Template not found: {name}")
110
+ return self._sources[name]
111
+
112
+ def _compute_hash(self, source: str) -> str:
113
+ """Compute SHA256 hash of template source."""
114
+ return hashlib.sha256(source.encode()).hexdigest()[:16]
115
+
116
+ def load_from_directory(
117
+ self,
118
+ directory: Path | str,
119
+ prefix: str = "",
120
+ extensions: tuple[str, ...] = (".jinja", ".j2", ".txt", ".md"),
121
+ ) -> int:
122
+ """
123
+ Load all templates from a directory.
124
+
125
+ Template names are derived from file paths:
126
+ - 'prompts/system/default.jinja' -> 'system.default'
127
+ - With prefix='prompts': 'prompts.system.default'
128
+
129
+ Args:
130
+ directory: Directory to load from
131
+ prefix: Prefix to add to template names
132
+ extensions: File extensions to load
133
+
134
+ Returns:
135
+ Number of templates loaded
136
+ """
137
+ directory = Path(directory)
138
+ if not directory.exists():
139
+ raise TemplateError(f"Template directory not found: {directory}")
140
+
141
+ count = 0
142
+ for file_path in directory.rglob("*"):
143
+ if file_path.is_file() and file_path.suffix in extensions:
144
+ # Convert path to template name
145
+ relative = file_path.relative_to(directory)
146
+ # Remove extension and convert path separators to dots
147
+ name_parts = list(relative.parts)
148
+ name_parts[-1] = relative.stem # Remove extension from last part
149
+ name = ".".join(name_parts)
150
+
151
+ if prefix:
152
+ name = f"{prefix}.{name}"
153
+
154
+ # Load and register
155
+ source = file_path.read_text()
156
+ self.register(name, source)
157
+ count += 1
158
+
159
+ return count
160
+
161
+ def clear(self) -> None:
162
+ """Remove all registered templates."""
163
+ self._templates.clear()
164
+ self._sources.clear()
165
+ self._hashes.clear()
@@ -0,0 +1,74 @@
1
+ """Tool/function calling helpers for Flashlite.
2
+
3
+ This module provides utilities for:
4
+ - Defining tools using decorators (@tool) or Pydantic models
5
+ - Converting tools to OpenAI/Anthropic formats
6
+ - Running tool execution loops for agentic patterns
7
+
8
+ Example:
9
+ from flashlite import Flashlite
10
+ from flashlite.tools import tool, run_tool_loop
11
+
12
+ @tool()
13
+ def get_weather(location: str, unit: str = "celsius") -> str:
14
+ '''Get the current weather for a location.'''
15
+ # Implementation
16
+ return f"Weather in {location}: 72°F"
17
+
18
+ @tool()
19
+ def search_web(query: str, max_results: int = 5) -> list[str]:
20
+ '''Search the web for information.'''
21
+ return ["result1", "result2"]
22
+
23
+ client = Flashlite(default_model="gpt-4o")
24
+
25
+ # Run tool loop
26
+ result = await run_tool_loop(
27
+ client=client,
28
+ messages=[{"role": "user", "content": "What's the weather in NYC?"}],
29
+ tools=[get_weather, search_web],
30
+ )
31
+ print(result.content)
32
+ """
33
+
34
+ from .definitions import (
35
+ ToolDefinition,
36
+ ToolRegistry,
37
+ format_tool_result,
38
+ get_tool_definition,
39
+ tool,
40
+ tool_from_pydantic,
41
+ tools_to_anthropic,
42
+ tools_to_openai,
43
+ )
44
+ from .execution import (
45
+ ToolCall,
46
+ ToolLoopResult,
47
+ ToolResult,
48
+ build_tool_registry,
49
+ execute_tool,
50
+ execute_tools_parallel,
51
+ extract_tool_calls,
52
+ run_tool_loop,
53
+ )
54
+
55
+ __all__ = [
56
+ # Definitions
57
+ "tool",
58
+ "tool_from_pydantic",
59
+ "ToolDefinition",
60
+ "ToolRegistry",
61
+ "get_tool_definition",
62
+ "tools_to_openai",
63
+ "tools_to_anthropic",
64
+ "format_tool_result",
65
+ # Execution
66
+ "ToolCall",
67
+ "ToolResult",
68
+ "ToolLoopResult",
69
+ "run_tool_loop",
70
+ "execute_tool",
71
+ "execute_tools_parallel",
72
+ "extract_tool_calls",
73
+ "build_tool_registry",
74
+ ]