File Coverage

blib/lib/Python/Unknown.pm
Criterion Covered Total %
statement 18 99 18.1
branch 0 38 0.0
condition 0 33 0.0
subroutine 6 8 75.0
pod 0 2 0.0
total 24 180 13.3


line stmt bran cond sub pod time code
1             #
2             # This file is part of App-PythonToPerl
3             #
4             # This software is Copyright (c) 2023 by Auto-Parallel Technologies, Inc.
5             #
6             # This is free software, licensed under:
7             #
8             # The GNU General Public License, Version 3, June 2007
9             #
10             # [[[ HEADER ]]]
11             # ABSTRACT: an unknown component
12             #use RPerl;
13             package Python::Unknown;
14 2     2   255598 use strict;
  2         11  
  2         65  
15 2     2   14 use warnings;
  2         11  
  2         90  
16             our $VERSION = 0.016_000;
17              
18             # [[[ OO INHERITANCE ]]]
19 2     2   19 use parent qw(Python::Component);
  2         9  
  2         21  
20 2     2   114 use Python::Component;
  2         4  
  2         92  
21              
22             # [[[ CRITICS ]]]
23             ## no critic qw(ProhibitUselessNoCritic ProhibitMagicNumbers RequireCheckedSyscalls) # USER DEFAULT 1: allow numeric values & print op
24             ## no critic qw(RequireInterpolationOfMetachars) # USER DEFAULT 2: allow single-quoted control characters & sigils
25             ## no critic qw(ProhibitConstantPragma ProhibitMagicNumbers) # USER DEFAULT 3: allow constants
26              
27             # [[[ INCLUDES ]]]
28 2     2   12 use Perl::Types;
  2         44  
  2         809  
29 2     2   16 use OpenAI::API;
  2         7  
  2         2766  
30              
31             # [[[ OO PROPERTIES ]]]
32             our hashref $properties = {
33             component_type => my string $TYPED_component_type = 'Python::Unknown',
34             sleep_seconds => my integer $TYPED_sleep_seconds = 5,
35             sleep_retry_multiplier => my number $TYPED_sleep_retry_multiplier = 1.5, # 1.5 == 150%, which means increase by 50%
36             retries_max => my integer $TYPED_retries_max = 10,
37             # all other properties inherited from Python::Component
38             };
39              
40             # [[[ SUBROUTINES & OO METHODS ]]]
41              
42             # PYUN01x
43             sub python_preparsed_to_perl_source {
44             # translate a chunk of Python source code into Perl source code
45 0     0 0   { my string $RETURN_TYPE };
  0            
46 0           ( my Python::Unknown $self, my OpenAI::API $openai ) = @ARG;
47              
48             # error if no OpenAI API
49 0 0         if (not defined $openai) {
50 0           croak 'ERROR EPYUN010: undefined OpenAI API, croaking';
51             }
52              
53             # error or warning if no Python source code
54 0 0 0       if ((not exists $self->{python_source_code}) or
      0        
55             (not defined $self->{python_source_code}) or
56             ($self->{python_source_code} eq q{})) {
57 0           croak 'ERROR EPYUN011: non-existent or undefined or empty Python source code, croaking';
58             }
59              
60             # DEV NOTE, PYUN012: $self->{python_preparsed} not used in this class, no need to error check
61              
62             # initialize property that will store de-parsed & translated source code;
63             # save fully translated Perl source code, to avoid repeated translating
64 0           $self->{perl_source_code_full} = '';
65              
66             # DEV NOTE: OpenAI::API rate limit is 20 requests per minute (RPM) for free tier, sleep to avoid hitting rate limit;
67             # DEV NOTE: sleep before API call instead of after call, to avoid "429 Too Many Requests" if previous call was recent
68 0           print 'in Python::Unknown::python_preparsed_to_perl_source(), about to call sleep(', $self->{sleep_seconds}, ')...', "\n";
69             # sleep before calling API
70 0           sleep($self->{sleep_seconds});
71 0           print 'in Python::Unknown::python_preparsed_to_perl_source(), ret from call to sleep(', $self->{sleep_seconds}, ')', "\n";
72              
73             # NEED ANSWER: the stop sequences were needed with Codex completions model 'code-davinci-002', do we still need now?
74             # NEED ANSWER: the stop sequences were needed with Codex completions model 'code-davinci-002', do we still need now?
75             # NEED ANSWER: the stop sequences were needed with Codex completions model 'code-davinci-002', do we still need now?
76              
77             # these strings signify an end to the translated Perl code, and the completion must stop when they are found
78 0           my string::arrayref $stop_sequences = ['# Python to', 'Python:'];
79              
80             # DEV NOTE: shorten error sequences to speed up matching and allow for new or different wording
81 0           my string::arrayref $error_sequences = [
82             'Sorry, ',
83             # 'Sorry, as an AI language model, I cannot see any Python source code to translate. Can you please provide the code so I can assist you better?',
84             # 'Sorry, I cannot perform this task as the provided Python source code is incomplete and does not provide enough information to be translated accurately. Please provide the complete code.',
85             ];
86              
87             # assemble "Python to Perl" message strings
88             my string::hashref::arrayref $messages = [
89             # DEV NOTE: save money by transmitting fewer tokens!
90             # { 'role' => 'system', 'content' => 'You are a helpful assistant with detailed knowledge of the Perl and Python computer programming languages, as well as modern software development best practices.' },
91             # { 'role' => 'user', 'content' => 'Translate the following Python source code to Perl:' . "\n" . $self->{python_source_code} . "\n" },
92 0           { 'role' => 'user', 'content' => 'Translate this Python to Perl:' . "\n" . $self->{python_source_code} . "\n" },
93             ];
94              
95 0           print 'in Python::Unknown::python_preparsed_to_perl_source(), have $self->{python_source_code} =', "\n", $self->{python_source_code}, "\n";
96 0           print 'in Python::Unknown::python_preparsed_to_perl_source(), about to call python_preparsed_to_perl_source_api_call()...', "\n";
97              
98             # call OpenAI API
99 0           my hashref $response = $self->python_preparsed_to_perl_source_api_call($openai, $stop_sequences, $messages);
100              
101 0           print 'in Python::Unknown::python_preparsed_to_perl_source(), ret from call to python_preparsed_to_perl_source_api_call()', "\n";
102             #print 'in Python::Unknown::python_preparsed_to_perl_source(), received $response = ', Dumper($response), "\n";
103             #die 'TMP DEBUG, UNKNOWN';
104              
105             # [[[ BEGIN FAILED API CALLS ]]]
106             # [[[ BEGIN FAILED API CALLS ]]]
107             # [[[ BEGIN FAILED API CALLS ]]]
108              
109             # handle HTTP response status codes returned from LWP::UserAgent
110 0 0 0       if ((defined $EVAL_ERROR) and ($EVAL_ERROR ne '')) {
111              
112             # NEED UPGRADE: accept any error code and just retry???
113             # NEED UPGRADE: accept any error code and just retry???
114             # NEED UPGRADE: accept any error code and just retry???
115              
116 0           print 'in Python::Unknown::python_preparsed_to_perl_source(), received $EVAL_ERROR = \'', $EVAL_ERROR, '\'', "\n";
117              
118             # Error retrieving 'completions': 400 Bad Request
119 0 0         if ($EVAL_ERROR =~ '400 Bad Request') {
    0          
    0          
120 0           croak 'ERROR EPYUN013a: received HTTP response status code 400 (bad request) instead of OpenAI::API response, croaking', "\n", $EVAL_ERROR, "\n";
121             }
122             # Error retrieving 'completions': 429 Too Many Requests
123             elsif ($EVAL_ERROR =~ '429 Too Many Requests') {
124 0           carp 'WARNING WPYUN013b: received HTTP response status code 429 (too many requests) instead of OpenAI::API response, carping', "\n", $EVAL_ERROR, "\n";
125              
126 0           my integer $retries = 0;
127              
128             # backoff algorithm; increase sleep time and retry
129             # https://help.openai.com/en/articles/5955604-how-can-i-solve-429-too-many-requests-errors
130             # https://platform.openai.com/docs/guides/rate-limits/error-mitigation
131 0   0       while ((defined $EVAL_ERROR) and ($EVAL_ERROR =~ '429 Too Many Requests')) {
132 0           print 'in Python::Unknown::python_preparsed_to_perl_source(), about to retry #', $retries, ' call python_preparsed_to_perl_source_api_call()...', "\n";
133 0           $retries++;
134 0 0         if ($retries >= $self->{retries_max}) {
135 0           croak 'ERROR EPYUN013b: received HTTP response status code 429 (too many requests) instead of OpenAI::API response, maximum retry limit ', $self->{retries_max}, ' reached, croaking';
136             }
137              
138             # increase time to sleep by some percentage of current amount
139 0           $self->{sleep_seconds} *= $self->{sleep_retry_multiplier};
140              
141 0           print 'in Python::Unknown::python_preparsed_to_perl_source(), about to retry #', $retries, ' call sleep(', $self->{sleep_seconds}, ')...', "\n";
142             # retry sleep before calling API
143 0           sleep($self->{sleep_seconds});
144 0           print 'in Python::Unknown::python_preparsed_to_perl_source(), ret from retry #', $retries, ' call to sleep(', $self->{sleep_seconds}, ')', "\n";
145              
146             # retry call to OpenAI API
147 0           $response = $self->python_preparsed_to_perl_source_api_call($openai, $messages);
148              
149 0           print 'in Python::Unknown::python_preparsed_to_perl_source(), ret from retry #', $retries, ' call to python_preparsed_to_perl_source_api_call()', "\n";
150             #print 'in Python::Unknown::python_preparsed_to_perl_source(), received $response = ', Dumper($response), "\n";
151             #die 'TMP DEBUG, UNKNOWN 429 TOO MANY REQUESTS';
152             }
153             }
154             # Error retrieving 'completions': 500 read timeout
155             elsif ($EVAL_ERROR =~ '500 read timeout') {
156 0           croak 'ERROR EPYUN013c: received HTTP response status code 500 (read timeout) instead of OpenAI::API response, croaking', "\n", $EVAL_ERROR, "\n";
157             # NEED SLEEP AND RETRY?
158             # NEED SLEEP AND RETRY?
159             # NEED SLEEP AND RETRY?
160             }
161             else {
162 0           croak 'ERROR EPYUN013d: received unrecognized HTTP response status code instead of OpenAI::API response, croaking', "\n", $EVAL_ERROR, "\n";
163             }
164             }
165              
166 0           print 'in Python::Unknown::python_preparsed_to_perl_source(), ret from call to OpenAI completions(), received $response =', "\n", Dumper($response), "\n";
167              
168             # DEV NOTE: sometimes the API returns an undefined or empty finish_reason, but still with valid 'text' field;
169             # carp & return dummy code if translation ends for any other reason than reaching a legitimate 'stop' condition
170 0 0 0       if (not defined $response) {
    0          
171 0           croak 'ERROR EPYUN014a: received undefined OpenAI::API response, croaking';
172             }
173             elsif ((not exists $response->{choices}) or (not defined $response->{choices}) or
174             (not defined $response->{choices}->[0])) {
175 0           croak 'ERROR EPYUN014b: received non-existent or undefined OpenAI::API response choice, croaking';
176             }
177             else {
178             # response choice is defined, check finish_reason and text
179 0 0 0       if ((not exists $response->{choices}->[0]->{finish_reason}) or (not defined $response->{choices}->[0]->{finish_reason})) {
    0          
180 0           carp 'WARNING WPYUN015a: received non-existent or undefined OpenAI::API response finish_reason, carping';
181             }
182             elsif ($response->{choices}->[0]->{finish_reason} ne 'stop') {
183 0           carp 'WARNING WPYUN015b: received OpenAI::API response finish_reason \'', $response->{choices}->[0]->{finish_reason}, '\', expected \'stop\', carping';
184             }
185              
186 0 0 0       if ((not exists $response->{choices}->[0]->{message}) or (not defined $response->{choices}->[0]->{message})) {
    0 0        
187 0           croak 'ERROR EPYUN016a: received non-existent or undefined OpenAI::API response message, croaking';
188             }
189             elsif ((not exists $response->{choices}->[0]->{message}->{content}) or (not defined $response->{choices}->[0]->{message}->{content})) {
190 0           croak 'ERROR EPYUN016b: received non-existent or undefined OpenAI::API response message content, croaking';
191             }
192             else {
193             # retrieve Perl source code out of valid response choice
194 0           $self->{perl_source_code} = $response->{choices}->[0]->{message}->{content};
195             }
196             }
197              
198             # translated Perl code must not start with any stop sequence,
199             # it should never have been returned from OpenAI::API in the first place
200 0           foreach my string $stop_sequence (@{$stop_sequences}) {
  0            
201 0           foreach my string $perl_source_code_line (split /\n/, $self->{perl_source_code}) {
202 0 0         if ($perl_source_code_line =~ m/^(\s*)$stop_sequence/) {
203 0           croak 'ERROR EPYUN017a: stop sequence encountered in OpenAI::API response text, API call malfunction, croaking';
204             }
205             }
206             }
207              
208             # translated Perl code must not start with any error sequence,
209             # must ensure transmitted Python code is complete & recognizable,
210             # if Python input code is good then we need to upgrade chunking or other App::PytonToPerl components
211 0           foreach my string $error_sequence (@{$error_sequences}) {
  0            
212 0           foreach my string $perl_source_code_line (split /\n/, $self->{perl_source_code}) {
213 0 0         if ($perl_source_code_line =~ m/^(\s*)$error_sequence/) {
214 0           croak 'ERROR EPYUN017b: error sequence encountered in OpenAI::API response text, API call malformation, croaking';
215             }
216             }
217             }
218              
219             # [[[ END FAILED API CALLS ]]]
220             # [[[ END FAILED API CALLS ]]]
221             # [[[ END FAILED API CALLS ]]]
222              
223             # remove all possibly-extraneous trailing blank lines and newlines returned by OpenAI::API response
224 0           while ( chomp $self->{perl_source_code} ) {
225 0           print 'in Python::Unknown->python_preparsed_to_perl_source(), chomping $self->{perl_source_code} before returning', "\n";
226 0           1; # dummy no-op, so while() loop body is never empty
227             }
228              
229             # START HERE: need fix indentation of returned Perl code
230             # START HERE: need fix indentation of returned Perl code
231             # START HERE: need fix indentation of returned Perl code
232              
233              
234             # NEED REMOVE DUMMY CODE!!!
235             # NEED REMOVE DUMMY CODE!!!
236             # NEED REMOVE DUMMY CODE!!!
237 0 0         if ($self->{perl_source_code} eq '') {
238 0           $self->{perl_source_code} = '# DUMMY PERL SOURCE CODE, NEED RETRY FAILED API CALL!'; # TEMPORARY DEBUG, NEED DELETE!
239             }
240              
241 0           print 'in Python::Unknown::python_preparsed_to_perl_source(), have $self->{perl_source_code} =', "\n", $self->{perl_source_code}, "\n";
242              
243             # return Perl source code
244 0           return $self->{perl_source_code};
245             }
246              
247              
248             # PYUN02x
249             sub python_preparsed_to_perl_source_api_call {
250             # call the OpenAI API
251 0     0 0   { my hashref $RETURN_TYPE };
  0            
252 0           ( my Python::Unknown $self, my OpenAI::API $openai, my string::arrayref $stop_sequences, my string::hashref::arrayref $messages ) = @ARG;
253 0           print 'in Python::Unknown::python_preparsed_to_perl_source_api_call(), received $self = ', Dumper($self), "\n";
254              
255             # error if no OpenAI API
256 0 0         if (not defined $openai) {
257 0           croak 'ERROR EPYUN020: undefined OpenAI API, croaking';
258             }
259              
260             # error or warning if no Python source code
261 0 0 0       if ((not exists $self->{python_source_code}) or
      0        
262             (not defined $self->{python_source_code}) or
263             ($self->{python_source_code} eq q{})) {
264 0           croak 'ERROR EPYUN021: non-existent or undefined or empty Python source code, croaking';
265             }
266              
267             # error if no API messages
268 0 0 0       if ((not defined $messages) or
269             (not defined $messages->[0])) {
270 0           croak 'ERROR EPYUN022: undefined API messages, croaking';
271             }
272              
273 0           print 'in Python::Unknown::python_preparsed_to_perl_source_api_call(), about to call OpenAI completions()...', "\n";
274              
275             # call OpenAI API, passing in Python source code and configuration options;
276             # receive back response, containing Perl source code and other info;
277             # wrap in eval{} to catch die() and enable checking $EVAL_ERROR below
278 0           my hashref $response;
279 0           eval { $response = $openai->chat(
  0            
280             # model => 'code-davinci-002', # RIP free beta 20230323 ~1615hrs CDT; death to OpenAI!
281             model => 'gpt-3.5-turbo',
282             # model => 'gpt-4', # wait until the price comes down, currently ~20x the price of gpt-3.5-turbo
283             messages => $messages,
284             temperature => 0,
285             max_tokens => 256,
286             top_p => 1,
287             frequency_penalty => 0,
288             presence_penalty => 0,
289             # DEV NOTE: the API spec & OpenAI::API allow the stop sequence to be a "string or array" which may contain "Up to 4 sequences"
290             # https://platform.openai.com/docs/api-reference/completions/create#completions/create-stop
291             # https://metacpan.org/pod/OpenAI::API
292             # DEV NOTE: translating Python `if` conditional block header without following body can result in `Python: ... Perl: ...` loop
293             stop => $stop_sequences,
294             ); };
295              
296 0           print 'in Python::Unknown::python_preparsed_to_perl_source_api_call(), received & about to return $response = ', Dumper($response), "\n";
297             #die 'TMP DEBUG, UNKNOWN OPENAI API';
298              
299 0           return $response;
300             }
301              
302             1;