Coverage for src/appl/core/function.py: 94%
84 statements
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-22 15:39 -0800
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-22 15:39 -0800
1from __future__ import annotations
3import inspect
4import sys
5import time
6import traceback
7from threading import Lock
8from typing import Any, Callable, Literal, Optional
10from .compile import appl_compile
11from .context import PromptContext
12from .modifiers import Compositor
15class PromptFunc:
16 """A wrapper for an APPL function, can be called as a normal function.
18 The function contains a prompt context, which could be same as or
19 copied from its caller function, or created from scratch, or resumed
20 from the last run.
21 """
23 def __init__(
24 self,
25 func: Callable,
26 ctx_method: str = "new",
27 comp: Optional[Compositor] = None,
28 default_return: Optional[Literal["prompt"]] = None,
29 include_docstring: bool = False,
30 new_ctx_func: Callable = PromptContext,
31 # default_sep: Optional[str] = None,
32 # ? set the default printer behavior for the prompt function?
33 ):
34 """Initialize the PromptFunc.
36 Args:
37 func (Callable): the function being wrapped
38 ctx_method (str):
39 the method to deal with the child context, available methods includes:
41 - (default) "new" or "new_ctx": create a brand new context.
42 - "copy" or "copy_ctx":
43 copy from the parent's context, the change will not
44 affect the parent's context.
45 - "same" or "same_ctx":
46 use the same context as the parent's, the change will
47 affect the parent's context.
48 - "resume" or "resume_ctx":
49 resume its own context from the last run.
50 For the first run, it will copy the parent's context.
52 comp (Compositor, optional):
53 the default compositor to be used. Defaults to None.
54 default_return (str, optional):
55 The default return value, "prompt" means return the prompt within
56 the function. Defaults to None.
57 include_docstring (bool, optional):
58 set to True to include the triple-quoted docstring in the prompt.
59 Defaults to False.
60 new_ctx_func (Callable, optional):
61 the function to create a new context. Defaults to PromptContext.
62 """
63 self._func = appl_compile(func)
64 self._signature = inspect.signature(func)
65 self._doc = func.__doc__
66 self._name = func.__name__
67 self._qualname = func.__qualname__
68 self._default_ctx_method = self._process_ctx_method(ctx_method)
69 self._default_compositor = comp
70 if default_return is not None and default_return != "prompt":
71 raise NotImplementedError("Only support default_return='prompt' now.")
72 self._default_return = default_return
73 self._include_docstring = include_docstring
74 self._new_ctx_func = new_ctx_func
75 self._persist_ctx: Optional[PromptContext] = None
76 self._reset_context_func: Optional[Callable[[], None]] = None
77 # self._default_sep = default_sep
79 @property
80 def compiled_func(self):
81 """The compiled function."""
82 return self._func
84 def _process_ctx_method(self, ctx_method: str) -> str:
85 available_methods = ["new", "copy", "same", "resume"]
86 alias = [m + "_ctx" for m in available_methods]
87 res = ctx_method
88 if res in alias:
89 res = available_methods[alias.index(ctx_method)]
90 if res not in available_methods:
91 raise ValueError(f"Unknown ctx_method: {ctx_method}")
92 return res
94 def _run(
95 self,
96 parent_ctx: PromptContext,
97 child_ctx: PromptContext,
98 *args: Any,
99 **kwargs: Any,
100 ) -> Any:
101 """Run the prompt function with desired context, deal with the exception."""
102 # if parent_ctx.is_outmost:
103 # exc = None
104 # try:
105 # results = self._func(_ctx=child_ctx, *args, **kwargs)
106 # except Exception:
107 # # if parent_ctx.is_outmost: # only print the trace in outmost appl function
108 # traceback.print_exc()
109 # print() # empty line
110 # exc = sys.exc_info()
112 # if exc is not None: # the outmost appl function
113 # _, e_instance, e_traceback = exc
114 # # already printed above, clean up the traceback
115 # if e_instance and e_traceback:
116 # e_traceback.tb_next = None
117 # raise e_instance.with_traceback(e_traceback)
118 # else: # run normally, capture the exception at the outmost appl function
119 # results = self._func(_ctx=child_ctx, *args, **kwargs)
120 results = self._func(_ctx=child_ctx, *args, **kwargs)
121 return results
123 def _call(
124 self, *args: Any, _ctx: Optional[PromptContext] = None, **kwargs: Any
125 ) -> Any:
126 """Call the prompt function."""
127 parent_ctx = _ctx or self._new_ctx_func()
128 is_class_method = kwargs.pop("_is_class_method", False)
129 ctx_method = kwargs.pop("ctx_method", self._default_ctx_method)
130 if ctx_method == "new":
131 child_ctx = self._new_ctx_func()
132 elif ctx_method == "copy":
133 child_ctx = parent_ctx.copy()
134 elif ctx_method == "same":
135 child_ctx = parent_ctx.inherit()
136 elif ctx_method == "resume":
137 # NOTE: the resume method is not thread-safe
138 # For standalone functions or class methods, they should de defined within the thread
139 # So that the function or `cls` is thread-local
140 # For object methods, the object need to be created within the thread
141 # So that `self` is thread-local
142 if is_class_method:
143 self_or_cls = args[0] # is it guaranteed? need double check
144 var_name = f"{self._name}_appl_ctx_"
146 def reset_context():
147 setattr(self_or_cls, var_name, None)
149 # try to retrieve the context from the class
150 if (ctx := getattr(self_or_cls, var_name, None)) is None:
151 # copy the parent context if not exist
152 child_ctx = parent_ctx.copy()
153 setattr(self_or_cls, var_name, child_ctx)
154 else:
155 # resume from the last run, but with clean locals
156 child_ctx = ctx.inherit()
157 else:
159 def reset_context():
160 self._persist_ctx = None
162 if self._persist_ctx is None:
163 self._persist_ctx = parent_ctx
165 child_ctx = self._persist_ctx.inherit()
167 self._reset_context_func = reset_context
168 else:
169 raise ValueError(f"Unknown ctx_method: {ctx_method}")
170 child_ctx.is_outmost = False
171 child_ctx._func_name = self._name
172 child_ctx._func_docstring = self._doc
173 child_ctx._docstring_quote_count = self._func._docstring_quote_count
174 child_ctx._include_docstring = self._include_docstring # set in the context
176 compositor: Optional[Compositor] = kwargs.pop(
177 "compositor", self._default_compositor
178 )
179 # push the compositor
180 if compositor is not None:
181 child_ctx.push_printer(compositor.push_args)
183 # run the function
184 results = self._run(parent_ctx, child_ctx, *args, **kwargs)
185 if results is None and self._default_return == "prompt":
186 results = child_ctx.records # the default return result
188 # pop the compositor
189 if compositor is not None:
190 child_ctx.pop_printer()
192 if ctx_method == "same":
193 parent_ctx.add_records(child_ctx.records, write_to_prompt=False)
195 return results
197 __call__ = _call
199 def __repr__(self):
200 res = f"[PromptFunc]{self._name}{self._signature}"
201 if self._doc is not None:
202 res += f": {self._doc}"
203 return res